Brand

Onboarding record: identity, voice, offerings, sources & intelligence.

This is not the live API. This page documents an earlier, in-browser mock API (routes like /accounts, /messaging and /brand), kept for reference. The real, read-only API that runs today is described in The API.

Brand — the single workspace record

The brand is the onboarding record the reveal step builds: identity, location, voice, offerings, channels per platform, memory rules, intelligence (competitors + targetCommunities), sources (indexed pages), and sourceUrl. Every route here reads or mutates the same in-memory BrandEntity (persisted to brand-entity in localStorage, migrated on load). The dashboard Brand tab, onboarding, and AI reply drafts all read this one record.

Seven routes: read the brand, upsert it (the workhorse), append intelligence (deduped), list/index/delete sources, and clear the whole record. Code: apps/web/src/lib/brand/server.ts and apps/web/src/lib/brand/types.ts (isBrandEntity, migrateBrandEntity).

GET /brand — Get brand

Returns the single workspace BrandEntity or null when onboarding was skipped or the brand was cleared. This is the source-of-truth the Brand tab, onboarding reveal, and AI reply drafts all read — polling here is how the dashboard knows whether to show the brand form. No parameters. Code: apps/web/src/lib/brand/server.ts:67

PUT /brand — Upsert brand

Creates the brand if none exists or deep-merges the Partial<BrandEntity> patch into the existing record. Merges identity, location, voice (formality/dos/donts/examples), channels per platform (facebook/x/reddit), offerings filtered by isBrandOffering, memory.rules, intelligence, and sources by URL. Stamps updatedAt, persists to brand-entity in localStorage, and validates the merged result with isBrandEntity — returns 400 Invalid brand payload if validation fails. Code: apps/web/src/lib/brand/server.ts:68

DELETE /brand — Delete brand

Clears the entire workspace brand record — identity, location, voice, offerings, sources, channels, memory, and intelligence are discarded (sets brand to null). Persists the cleared state so the dashboard returns to the onboarding prompt. Idempotent. Code: apps/web/src/lib/brand/server.ts:186

POST /brand/intelligence — Append brand intelligence

Appends reveal discoveries to the brand's intelligence block. competitors are deduped by exact string (trimmed), targetCommunities are merged by id via isCommunityPick, and selectedKeyword overwrites if provided. Returns 404 No brand yet if onboarding hasn't created a record yet. Stamps updatedAt and persists. Code: apps/web/src/lib/brand/server.ts:113

GET /brand/sources — List brand sources

Returns the indexed BrandPage[] for the brand's website — each page has url, title, headings, text, status (indexed/pending/failed), and fetchedAt. Returns an empty array when no brand exists yet (the brand itself is the guard, not the sources). Code: apps/web/src/lib/brand/server.ts:142

DELETE /brand/sources — Delete brand source

Removes a single indexed page by exact url string match. Filters brand.sources by URL, stamps updatedAt, and persists. Returns 404 if no brand exists — the URL itself is not validated beyond string match (unknown URLs are a no-op that still returns the filtered list). Code: apps/web/src/lib/brand/server.ts:178

POST /brand/index — Index brand site

Mock sitemap indexer. With { urls: string[] } it registers each new URL as a pending BrandPage (deduped by URL). Without urls (or with { sitemap: true } / empty body) it re-resolves the brand site (identity.website or sourceUrl) into the deterministic seed pages via resolveSitemap and merges them by URL as indexed. Always stamps and persists. Returns 404 if no brand exists. Code: apps/web/src/lib/brand/server.ts:143

Get brand

Returns the single workspace BrandEntity or null when onboarding was skipped or the brand was cleared. This is the source-of-truth the Brand tab, onboarding reveal, and AI reply drafts all read — polling here is how the dashboard knows whether to show the brand form. No parameters. Code: apps/web/src/lib/brand/server.ts:67

GET/brand

Response Body

Brand record (or null).

TypeScript Definitions

Use the response body type in TypeScript.

brandRequiredobject | null | null
curl -X GET "http://localhost:5174/brand"
fetch("http://localhost:5174/brand")
package mainimport (  "fmt"  "net/http"  "io/ioutil")func main() {  url := "http://localhost:5174/brand"  req, _ := http.NewRequest("GET", url, nil)    res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand"response = requests.request("GET", url)print(response.text)
{
  "brand": {
    "property1": null,
    "property2": null
  }
}

Delete brand

Clears the entire workspace brand record — identity, location, voice, offerings, sources, channels, memory, and intelligence are discarded (sets brand to null). Persists the cleared state so the dashboard returns to the onboarding prompt. Idempotent. Code: apps/web/src/lib/brand/server.ts:186

DELETE/brand

Response Body

Cleared (brand is null).

TypeScript Definitions

Use the response body type in TypeScript.

brandRequiredobject | null | null
curl -X DELETE "http://localhost:5174/brand"
fetch("http://localhost:5174/brand")
package mainimport (  "fmt"  "net/http"  "io/ioutil")func main() {  url := "http://localhost:5174/brand"  req, _ := http.NewRequest("DELETE", url, nil)    res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand"response = requests.request("DELETE", url)print(response.text)
{
  "brand": {
    "property1": null,
    "property2": null
  }
}

Upsert brand

Creates the brand if none exists or deep-merges the Partial<BrandEntity> patch into the existing record. Merges identity, location, voice (formality/dos/donts/examples), channels per platform (facebook/x/reddit), offerings filtered by isBrandOffering, memory.rules, intelligence, and sources by URL. Stamps updatedAt, persists to brand-entity in localStorage, and validates the merged result with isBrandEntity — returns 400 Invalid brand payload if validation fails. Code: apps/web/src/lib/brand/server.ts:68

PUT/brand

Request Body

application/jsonRequired

Response Body

Saved brand.

TypeScript Definitions

Use the response body type in TypeScript.

brandRequiredobject | null | null

Failure envelope returned with 4xx statuses.

TypeScript Definitions

Use the response body type in TypeScript.

errorRequiredstring

Human-readable failure reason, e.g. "A key needs a name".

curl -X PUT "http://localhost:5174/brand" \  -H "Content-Type: application/json" \  -d '{}'
const body = JSON.stringify({})fetch("http://localhost:5174/brand", {  body})
package mainimport (  "fmt"  "net/http"  "io/ioutil"  "strings")func main() {  url := "http://localhost:5174/brand"  body := strings.NewReader(`{}`)  req, _ := http.NewRequest("PUT", url, body)  req.Header.Add("Content-Type", "application/json")  res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand"body = {}response = requests.request("PUT", url, json = body, headers = {  "Content-Type": "application/json"})print(response.text)
{
  "brand": {
    "property1": null,
    "property2": null
  }
}
{
  "error": "string"
}

Append brand intelligence

Appends reveal discoveries to the brand's intelligence block. competitors are deduped by exact string (trimmed), targetCommunities are merged by id via isCommunityPick, and selectedKeyword overwrites if provided. Returns 404 No brand yet if onboarding hasn't created a record yet. Stamps updatedAt and persists. Code: apps/web/src/lib/brand/server.ts:113

POST/brand/intelligence

Request Body

application/jsonRequired
competitorsarray<string>

Competitor domains to append (deduped).

targetCommunitiesarray<object>

CommunityPick[] merged by id.

selectedKeywordstring

Keyword chosen in reveal.

Response Body

Updated brand.

TypeScript Definitions

Use the response body type in TypeScript.

brandRequiredobject | null | null

Failure envelope returned with 4xx statuses.

TypeScript Definitions

Use the response body type in TypeScript.

errorRequiredstring

Human-readable failure reason, e.g. "A key needs a name".

Failure envelope returned with 4xx statuses.

TypeScript Definitions

Use the response body type in TypeScript.

errorRequiredstring

Human-readable failure reason, e.g. "A key needs a name".

curl -X POST "http://localhost:5174/brand/intelligence" \  -H "Content-Type: application/json" \  -d '{    "competitors": [      "string"    ],    "targetCommunities": [      {}    ],    "selectedKeyword": "string"  }'
const body = JSON.stringify({  "competitors": [    "string"  ],  "targetCommunities": [    {}  ],  "selectedKeyword": "string"})fetch("http://localhost:5174/brand/intelligence", {  body})
package mainimport (  "fmt"  "net/http"  "io/ioutil"  "strings")func main() {  url := "http://localhost:5174/brand/intelligence"  body := strings.NewReader(`{    "competitors": [      "string"    ],    "targetCommunities": [      {}    ],    "selectedKeyword": "string"  }`)  req, _ := http.NewRequest("POST", url, body)  req.Header.Add("Content-Type", "application/json")  res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand/intelligence"body = {  "competitors": [    "string"  ],  "targetCommunities": [    {}  ],  "selectedKeyword": "string"}response = requests.request("POST", url, json = body, headers = {  "Content-Type": "application/json"})print(response.text)
{
  "brand": {
    "property1": null,
    "property2": null
  }
}
{
  "error": "string"
}
{
  "error": "string"
}

List brand sources

Returns the indexed BrandPage[] for the brand's website — each page has url, title, headings, text, status (indexed/pending/failed), and fetchedAt. Returns an empty array when no brand exists yet (the brand itself is the guard, not the sources). Code: apps/web/src/lib/brand/server.ts:142

GET/brand/sources

Response Body

Indexed pages.

TypeScript Definitions

Use the response body type in TypeScript.

sourcesRequiredarray<object>
curl -X GET "http://localhost:5174/brand/sources"
fetch("http://localhost:5174/brand/sources")
package mainimport (  "fmt"  "net/http"  "io/ioutil")func main() {  url := "http://localhost:5174/brand/sources"  req, _ := http.NewRequest("GET", url, nil)    res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand/sources"response = requests.request("GET", url)print(response.text)
{
  "sources": [
    {
      "property1": null,
      "property2": null
    }
  ]
}

Delete brand source

Removes a single indexed page by exact url string match. Filters brand.sources by URL, stamps updatedAt, and persists. Returns 404 if no brand exists — the URL itself is not validated beyond string match (unknown URLs are a no-op that still returns the filtered list). Code: apps/web/src/lib/brand/server.ts:178

DELETE/brand/sources

Request Body

application/jsonRequired
urlRequiredstring

Exact URL of the page to remove.

Response Body

Remaining sources.

TypeScript Definitions

Use the response body type in TypeScript.

brandRequiredobject

BrandEntity — single workspace brand record (see docs: Brand).

sourcesRequiredarray<object>

Failure envelope returned with 4xx statuses.

TypeScript Definitions

Use the response body type in TypeScript.

errorRequiredstring

Human-readable failure reason, e.g. "A key needs a name".

curl -X DELETE "http://localhost:5174/brand/sources" \  -H "Content-Type: application/json" \  -d '{    "url": "string"  }'
const body = JSON.stringify({  "url": "string"})fetch("http://localhost:5174/brand/sources", {  body})
package mainimport (  "fmt"  "net/http"  "io/ioutil"  "strings")func main() {  url := "http://localhost:5174/brand/sources"  body := strings.NewReader(`{    "url": "string"  }`)  req, _ := http.NewRequest("DELETE", url, body)  req.Header.Add("Content-Type", "application/json")  res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand/sources"body = {  "url": "string"}response = requests.request("DELETE", url, json = body, headers = {  "Content-Type": "application/json"})print(response.text)
{
  "brand": {
    "property1": null,
    "property2": null
  },
  "sources": [
    {
      "property1": null,
      "property2": null
    }
  ]
}
{
  "error": "string"
}

Index brand site

Mock sitemap indexer. With { urls: string[] } it registers each new URL as a pending BrandPage (deduped by URL). Without urls (or with { sitemap: true } / empty body) it re-resolves the brand site (identity.website or sourceUrl) into the deterministic seed pages via resolveSitemap and merges them by URL as indexed. Always stamps and persists. Returns 404 if no brand exists. Code: apps/web/src/lib/brand/server.ts:143

POST/brand/index

Request Body

application/jsonOptional
urlsarray<string>

Explicit URLs to register as pending.

sitemapboolean

When true (or no urls), re-resolve the sitemap.

Response Body

Indexed pages.

TypeScript Definitions

Use the response body type in TypeScript.

brandRequiredobject

BrandEntity — single workspace brand record (see docs: Brand).

sourcesRequiredarray<object>

Failure envelope returned with 4xx statuses.

TypeScript Definitions

Use the response body type in TypeScript.

errorRequiredstring

Human-readable failure reason, e.g. "A key needs a name".

curl -X POST "http://localhost:5174/brand/index" \  -H "Content-Type: application/json" \  -d '{    "urls": [      "string"    ],    "sitemap": true  }'
const body = JSON.stringify({  "urls": [    "string"  ],  "sitemap": true})fetch("http://localhost:5174/brand/index", {  body})
package mainimport (  "fmt"  "net/http"  "io/ioutil"  "strings")func main() {  url := "http://localhost:5174/brand/index"  body := strings.NewReader(`{    "urls": [      "string"    ],    "sitemap": true  }`)  req, _ := http.NewRequest("POST", url, body)  req.Header.Add("Content-Type", "application/json")  res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "http://localhost:5174/brand/index"body = {  "urls": [    "string"  ],  "sitemap": true}response = requests.request("POST", url, json = body, headers = {  "Content-Type": "application/json"})print(response.text)
{
  "brand": {
    "property1": null,
    "property2": null
  },
  "sources": [
    {
      "property1": null,
      "property2": null
    }
  ]
}
{
  "error": "string"
}