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.