Brand intelligence
The repository README contains the owned GitHub core entry point into this
loop, with bounded source=github, utm_medium=owned, and
utm_campaign=core attribution.
This page is the canonical public contract for Brand Intelligence, including the v0.1.1 external npm CLI aggregate read. It describes two intentionally different surfaces:
- The authenticated, metered
GET /v1/brand/retrieve,GET /v1/brand/search,GET /v1/brand/logo,GET /v1/brand/fonts, andGET /v1/brand/styleguideroutes onapi.replynodes.com. - The one anonymous aggregate read on the dedicated host:
GET https://brand.replynodes.com/{bare-domain}.
The canonical OpenAPI contract remains
services/replynodes-fetcher/internal/gateway/capabilities.openapi.json
for the /v1 routes: it is the source of their paths, parameters, schemas,
security, and standard {data, meta} response envelope. The dedicated-host
aggregate is intentionally not an anonymous /v1 route and is specified here
because it has a separate path grammar, direct response object, quota, and
cache policy. GET /v1/webcontext/brand remains a separate authenticated,
metered operation governed by OpenAPI. None of these /v1 routes is anonymous;
do not infer anonymous access to any /v1/brand/* route from the aggregate
contract.
Brand Kit CLI
The approved v0.1.1 npm release target is the scoped package
@replynodes/brand-kit. Run it without an install using:
npx @replynodes/brand-kit <bare-domain>The installed executable remains brand-kit; the unscoped brand-kit package name was blocked by
npm similarity policy because it was too similar to the existing brandkit
package. The GitHub repository remains replynodes/brand-kit, and the runtime
endpoint remains GET https://brand.replynodes.com/{bare-domain}. No API key or
signup is required, and the CLI preserves one request and read-only behavior.
The old unpublished GitHub v0.1.0 release/tag is historical. The approved
v0.1.1 package is published and current; a separate v0.1.2 keyword patch is
also published.
Anonymous aggregate endpoint
The v0.1.1 CLI uses exactly this endpoint:
GET https://brand.replynodes.com/{bare-domain}It requires no account and no API key. The path value is one bare, public DNS domain. An optional single trailing slash is accepted. The query string is ignored, so it must not be used for versioning or options.
Normalization is deterministic: trim surrounding whitespace, lowercase, strip
one leading www., and strip one trailing DNS dot. Embedded whitespace and
control characters are rejected rather than trimmed. The resulting hostname
must be an ASCII hostname of at least two dot-separated labels and no more than
253 characters total; each label is 1–63 characters, starts and ends with an
ASCII letter or digit, and permits hyphens only in the interior. Normalization
is used for the upstream lookup and cache key.
Accepted examples:
/stripe.com
/WWW.Stripe.com/
/stripe.com.
/stripe.com?anything=ignoredRejected examples (all return 400):
/https://stripe.com # scheme
/stripe.com:443 # port
/stripe.com/docs # path
/@user:password@stripe.com # credentials
/localhost # local name
/127.0.0.1 # IP literal
/169.254.169.254 # metadata/private addressCanonical and non-canonical IP spellings, cloud-metadata names, malformed labels, and whitespace/control characters are rejected before an upstream brand fetch. The resolution-level SSRF guard validates the initial DNS resolution and rejects a non-public address. The pinned runtime then performs its own separate fetch/resolution, so a residual DNS-rebinding TOCTOU risk remains; this guard is not absolute protection against rebinding.
Successful response
The response is a direct JSON object with
Content-Type: application/json; charset=utf-8. It is not wrapped in
data. Unknown keys must be ignored by clients.
The exact top-level keys are:
| Key | Type | Presence and meaning |
|---|---|---|
domain | string | Always present; normalized lookup domain. |
url | string | Always present; canonical target URL used for extraction. |
name | string | Optional detected brand/site name. |
description | string | Optional public description. |
favicon | string | Optional favicon URL. |
og_image | string | Optional Open Graph/image URL. |
primary_logo | string | Optional primary logo URL or inline asset value. |
logos | array of objects | Optional logo assets. Each item has url (string) and may have kind (string). |
colors | array of objects | Optional colors. Each item has hex (string), and may have usage (string) and count (integer). |
fonts | array of strings | Optional detected font families. |
social_links | array of strings | Optional links from supported social hosts. |
backdrops | array of objects | Optional hero/banner/cover assets, with the same item shape as logos. |
styleguide | object | Optional merged styleguide; see below. |
meta | object | Always present; aggregate response metadata. |
Absent fields are normal. An absent field means the public source did not provide a usable value; it is not an error. Clients must not assume that an empty logo, color, font, social-link, backdrop, or styleguide collection is available, and should generate local outputs around whatever subset is present.
meta has these keys and types:
| Key | Type | Meaning |
|---|---|---|
domain | string | Same normalized domain as the top-level domain. |
cached | boolean | false for a fresh aggregate build; true when served from the shared cache. |
fetched_at | string | UTC RFC 3339 timestamp for the cached/fresh aggregate build. |
cache_ttl_seconds | integer | 86400 for successful aggregate responses. |
source | string | brand-intelligence. |
docs | string | Canonical documentation URL. |
Styleguide
When styleguide extraction succeeds, styleguide is an object containing
domain (string), url (string), status (string), and any available
mode, colors, typography, buttons, card, spacing, shadows, and
radii fields. Those nested collections and properties are optional. The
status values are:
extracted: usable CSS/style blocks and design signals were extracted.partial: some style signals were found, but the result is incomplete.unavailable: the page was reachable but no usable styleguide signals were available.
If the separate styleguide fetch itself fails, the aggregate still returns
the successfully retrieved brand fields and omits the styleguide key. A
styleguide failure does not turn an otherwise successful brand aggregate into
an error. A present styleguide with status: "unavailable" is a valid
successful result and is different from a failed styleguide fetch.
Asset kinds and values
Every logos[].kind and backdrops[].kind is normalized from an extraction
slot and an asset format. When the slot is non-empty and differs from the
format, the result is <slot>.<format>; when the slot is empty, the format is
returned alone; and when the slot already equals the format, the slot-only
value is returned (for example, svg). For HTTP(S) URLs, the format is taken
from the URL path and is one of ico, png, svg, jpg, jpeg, webp,
avif, gif, or the fallback image. Non-HTTP(S) values, including inline
assets, use the inline format. Thus different files from the same slot
remain distinguishable, such as favicon.svg, favicon.ico, and
apple-touch-icon.png, while slot/format combinations follow the rules above.
Treat non-HTTP(S) values as opaque data/references: do not fetch them as URLs.
Only fetch or download asset values beginning with http:// or https://;
preserve other values for a later local rendering decision.
HTTP behavior and errors
The aggregate supports GET and HEAD. HEAD performs the same contract
checks and reports the Content-Length of the corresponding GET JSON
representation, but sends no response body. Successful JSON responses include
Cache-Control: public, max-age=86400, and Access-Control-Allow-Origin: *.
Other methods return 405 with Allow: GET, HEAD. Errors use the standard
object envelope:
{
"error": {
"code": "invalid_request",
"message": "The request is malformed or contains invalid parameters.",
"request_id": "..."
}
}The contract-level statuses are:
| Status | Code | Exact message |
|---|---|---|
400 | invalid_request | The request is malformed or contains invalid parameters. |
405 | method_not_allowed | This endpoint only supports GET requests. |
404 | not_found | The requested resource was not found. |
429 | rate_limited | The rate limit for this workspace or API key has been exhausted. |
502 | upstream_unavailable | The data provider is temporarily unavailable. |
503 | degraded | Authorization is temporarily unavailable; try again shortly. |
504 | gateway_timeout | The request could not be completed within the allowed time. |
The request_id value is deployment-generated and should be retained for
support or diagnostics. HEAD sends no response body; clients should use the
status and headers.
The 404 not_found case means the dedicated route is unavailable or not
configured on that deployment; it does not indicate invalid input.
Cache, quota, and failure behavior
The anonymous quota is the shared capability-aware Tier A quota for the
canonical brand-kit capability: a fixed 20 requests per UTC calendar day
per trusted client-IP bucket. The bucket identifier is derived from the
trusted client IP, the canonical capability, and the UTC date, and it lives in
the shared Redis counter namespace — the same engine and storage used by the
anonymous Markdown route, not a separate provider-specific minute counter.
Domain validation runs before admission, so a malformed or rejected request is
answered with 400 and never consumes a quota unit. Once admitted, a cache hit
and an upstream failure each consume exactly one daily unit. Successful
aggregate responses are stored in the shared Redis cache for 24 hours.
X-Cache: miss identifies a fresh build and X-Cache: hit identifies a cached
response.
Responses expose:
X-RateLimit-Limit: 20
X-RateLimit-Remaining: <integer remaining in the current UTC day>
X-RateLimit-Reset: <Unix timestamp of the next UTC midnight>When the quota is exhausted, the response is 429 and includes
Retry-After in seconds until the next UTC midnight.
A separate, generic per-IP burst safeguard of 20 requests per minute also
applies to every brand-host request, anonymous or keyed. It is not product
quota: it never consults or decrements the brand-kit capability bucket, it is
not metered or billed, and when it trips the route returns the generic
429 rate_limited (with Retry-After), never anonymous_limit_reached. It
absorbs abusive bursts, including from keyed callers who are not subject to the
anonymous daily quota; the X-RateLimit-* headers describe the product quota,
not this generic safeguard.
Missing, unavailable, or failed Redis is fail-closed: the route returns
503 degraded rather than
using a process-local limiter, bypassing the cache, or serving an uncached
anonymous upstream request. Cache read and write failures are also degraded
responses.
The gateway’s shipped default outer handler timeout is 15 seconds. Normally,
the configured 15-second handler timeout yields a 12-second brand-fetch
budget. Otherwise, subtract 3 seconds from the configured handler timeout,
apply a 5-second floor, then cap the result at the outer handler timeout, so
configured outer handler timeouts below 5 seconds yield that outer timeout. A
non-positive configuration falls back to the shipped 15-second default. The
subsequent cache SET uses context.WithoutCancel with no
separate deadline, so it is not guaranteed to finish before the outer
deadline; an outer handler timeout can still yield 504 gateway_timeout.
Upstream failure maps to 502 upstream_unavailable; a fetch or request
deadline maps to 504 gateway_timeout; and cache read or write failure maps
to 503 degraded. SSRF and public-address checks apply before upstream
extraction, subject to the residual DNS-rebinding risk described above. No
provider credentials, cookies, sessions, or write operation are part of this
endpoint.
Verification and evidence
Production verification can use safe, read-only requests:
curl -sS -D /tmp/brand.headers \
-o /tmp/brand.json \
'https://brand.replynodes.com/stripe.com'
curl -sS -I 'https://brand.replynodes.com/stripe.com'
curl -sS -D - -o /dev/null \
'https://brand.replynodes.com/stripe.com?probe=ignored'Confirm the status, JSON content type, domain/url/meta, cache and rate
limit headers, and that the HEAD response has no body while its
Content-Length matches the corresponding GET representation. A second GET
for the same normalized domain should be a cache hit when the cache is warm.
Do not send API keys or other secrets. When recording evidence, redact client
IP details, cookies, authorization material, internal hostnames, and any
unbounded request or response metadata; retain status, relevant public
headers, request ID, and a redacted response shape.
This repository currently defines the production hostname
brand.replynodes.com; it does not define a public staging or testnet
brand hostname. Do not invent one. For a staging/testnet deployment, an
operator must first provide the deployment’s explicitly configured public
brand host, then run the same safe checks against that host and record the
configured host and redacted evidence. Internal service addresses are not
public CLI endpoints.
Compatibility and CLI boundary
The external npm CLI v0.1 should treat this direct object as an additive,
forward-compatible contract: tolerate omitted optional fields and unknown
future fields, and require only domain, url, and meta for a structurally
valid success. Additive fields do not require a new CLI endpoint or a new
versioned path. There is no query-based API versioning and no API-key mode for
the aggregate endpoint.
After fetching, CLI outputs are generated locally. These are CLI concerns, not additional API response contracts:
brand.json
colors.json
fonts.json
logo references
tokens.css
DESIGN.mdNon-goals
- No anonymous
/v1brand routes. - No account or API key for the aggregate read.
- No new aggregation backend service or endpoint.
- No change to the authenticated/metered
/v1/brand/retrieve,/v1/brand/search,/v1/brand/logo,/v1/brand/fonts, or/v1/brand/styleguideroutes. - No server-side generation of the npm CLI’s local artifacts.
Cross-discovery and returning-caller CTA
Successful anonymous aggregate responses include
X-ReplyNodes-Discovery: markdown and a Link relation to
https://md.replynodes.com/{domain}. A valid returning
rn_anon_caller_id cookie also exposes X-ReplyNodes-CTA: api-key and a
Link relation to https://platform.replynodes.com/sign-up. These are
informational response headers; no signup or API key is required before the
free read.