ReplyNodes
Guides

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, and GET /v1/brand/styleguide routes on api.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=ignored

Rejected 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 address

Canonical 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:

KeyTypePresence and meaning
domainstringAlways present; normalized lookup domain.
urlstringAlways present; canonical target URL used for extraction.
namestringOptional detected brand/site name.
descriptionstringOptional public description.
faviconstringOptional favicon URL.
og_imagestringOptional Open Graph/image URL.
primary_logostringOptional primary logo URL or inline asset value.
logosarray of objectsOptional logo assets. Each item has url (string) and may have kind (string).
colorsarray of objectsOptional colors. Each item has hex (string), and may have usage (string) and count (integer).
fontsarray of stringsOptional detected font families.
social_linksarray of stringsOptional links from supported social hosts.
backdropsarray of objectsOptional hero/banner/cover assets, with the same item shape as logos.
styleguideobjectOptional merged styleguide; see below.
metaobjectAlways 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:

KeyTypeMeaning
domainstringSame normalized domain as the top-level domain.
cachedbooleanfalse for a fresh aggregate build; true when served from the shared cache.
fetched_atstringUTC RFC 3339 timestamp for the cached/fresh aggregate build.
cache_ttl_secondsinteger86400 for successful aggregate responses.
sourcestringbrand-intelligence.
docsstringCanonical 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:

StatusCodeExact message
400invalid_requestThe request is malformed or contains invalid parameters.
405method_not_allowedThis endpoint only supports GET requests.
404not_foundThe requested resource was not found.
429rate_limitedThe rate limit for this workspace or API key has been exhausted.
502upstream_unavailableThe data provider is temporarily unavailable.
503degradedAuthorization is temporarily unavailable; try again shortly.
504gateway_timeoutThe 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.md

Non-goals

  • No anonymous /v1 brand 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/styleguide routes.
  • 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.

On this page