Skip to content

Internal API

Brand Protect is two services. This page documents the internal REST contract between them. All endpoints require the shared INTERNAL_API_TOKEN as a bearer token (Rails additionally skips CSRF for internal calls).

Base URL: PYTHON_API_URL (default http://python:8000/api/v1). Calls are authenticated with Authorization: Bearer $INTERNAL_API_TOKEN. Timeouts: 5s open, 15s read (180s for PDF binaries).

Method Path Purpose
POST /scan/start Start a scan. Body: {scan_id, user_id, brand_name, keywords, owned_domains, social_handles, logo_url, domain_profiles, resume_from_stage} → 202
POST /campaign/narratives LLM narratives for campaign clusters. Body: {scan_id}
POST /takedown/generate Takedown draft. Body: {scan_id, finding_id, domain}{subject, body}
POST /domain/enrich Enrich an owned domain. Body: {owned_domain_id, user_id, domain, generation}
POST /social/enrich Enrich a social handle. Body: {social_handle_id, user_id, platform, handle, generation}
POST /brand/keyword_expand Expand brand keywords. Body: {user_id, generation}
POST /report/generate Generate PDFs. Body: {scan_id}{full, summary} (base64)

Base URL: RAILS_API_URL (default http://rails:80). Routes live under V1::InternalApiController; tokens are compared with ActiveSupport::SecurityUtils.secure_compare and fail closed (401) when INTERNAL_API_TOKEN is unset.

Method Path Purpose
GET /v1/scoring/weights Current scoring weights {version, weights}
PUT /v1/scoring/weights Store weights; version = max(latest+1, requested, 1)
GET /v1/scoring/feedback_samples Analyst labels (max 500): {label, reason, signals, ai_verdict, ai_confidence, adjusted_score, created_at}
GET /v1/scoring/campaign_stats Campaign clusters, last 30 days (max 2000): [{size, domains, keys, earliest_registration, latest_registration}]
Method Path Purpose
GET /v1/settings/ai_provider {enabled, config}; enabled is null when unconfigured
PUT /v1/settings/ai_provider Deep-merge the ai_provider setting
GET /v1/settings/audit_config {threshold: "medium", adjustment_min: -15, adjustment_max: 15}
PUT /v1/settings/audit_config Deep-merge audit configuration
Method Path Purpose
GET /v1/users/:id/derived_keywords Current derived keywords
POST /v1/users/:id/derived_keywords Persist derived keywords (deduped, downcased); 409 on stale generation, 422 on bad payload
GET /v1/users/:id/brand_context {brand_name, social_handles, keywords, derived_keywords, owned_domains: [{domain, title, content_snippet}]}
Method Path Purpose
PUT /v1/scans/:id/audit Persist an AI audit verdict (validated); recomputes adjusted_score + adjusted_severity
GET /v1/scans/:id/audit_data Full audit snapshot: {scan, brand, findings (with screenshot_data), social_findings}; 404 when missing
  • scan/start returns 202 — work runs in background tasks.
  • logo_url in audit data is built from RAILS_PUBLIC_HOST; logo_data_uri is base64 for PDFs.
  • Errors surface via the Redis event stream rather than blocking the caller.