Skip to main content
RepScale iconREPSCALE
REST API reference

RepScale REST API.

A REST surface over RepScale's research engine. Same code path as the web app and the MCP server. Pick whichever wire format your stack prefers. API keys, rate limits, and telemetry are shared across all three.

Pro and Enterprise only. Free-plan keys receive 403 plan_required on every endpoint.

Generate an API key

Setup

Base URL.

https://repscale.ai

All paths below are relative to the base URL. Current version is v1. When v2 ships, v1 stays frozen and supported on the same path.

Authentication

Authenticate with an API key.

Every request requires a RepScale API key. The same keys work across the web app, MCP, and REST.

Format: rsk_live_<32 hex chars>

Two ways to send it. Pick whichever fits your stack:

HTTP
Authorization: Bearer rsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
HTTP
x-api-key: rsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Getting a key

  1. Sign in at repscale.ai
  2. Go to Settings → API Keys
  3. Generate a key and copy the plaintext. It's shown exactly once.

Revoke from the same page. Revocation takes effect immediately.

Auth failures

ConditionStatuserror code
Missing or malformed Authorization header401unauthorized
Key not found, revoked, or for a deleted user401unauthorized
Valid key on a Free plan403plan_required

Reference

Endpoints.

All five endpoints are versioned under /api/v1/.

MethodPathDescription
POST/api/v1/researchGenerate a research brief. SSE-streamed.
GET/api/v1/research/{id}/anglesRecommended sales angle, what-to-avoid list, opening question.
GET/api/v1/research/{id}/confidenceWhy the brief is low/medium/high confidence.
GET/api/v1/research/{id}/adjacentRanked adjacent candidates: other execs, vendors, initiatives.
POST/api/v1/research/{id}/citeSource URL + excerpt + date for a specific claim.
GET/api/v1/openapiOpenAPI 3.1 spec. No auth, public.

{id} in the URL is the brief_id returned by a prior research call.

POST /api/v1/research

Generates a brief for a prospect. Long-running. Typical latency is 60 to 120 seconds on a cold cache because the underlying call performs live web search. The response is wrapped in an SSE keepalive stream. See Streaming format below. Server-side 7-day cache: if you've already researched the same name + company within 7 days, the cached brief is returned instantly with cached: true.

Request body:

JSON
{
  "name": "Jane Doe",
  "company": "Acme Corp",
  "title": "VP of Operations",
  "website": "https://acme.com",
  "context": "Met at SaaStr 2025; mentioned fleet visibility pain."
}
FieldTypeRequiredNotes
namestringyesFull name of the prospect. Max 200 chars.
companystringyesCompany name. Max 300 chars.
titlestringnoJob title. Improves the brief.
websitestringnoCompany URL. Used as the primary first-party source.
contextstringnoSeed context: call notes, LinkedIn bio. Max 5000 chars. Treated as input, not echoed back.

Response body. The JSON inside the final data: SSE frame:

JSON
{
  "cached": false,
  "prospect": {
    "name": "Jane Doe",
    "title": "VP of Operations",
    "linkedin_url": null
  },
  "contact_insights": {
    "background": "...",
    "communication_style": "...",
    "likely_priorities": ["..."],
    "approach_notes": "...",
    "personal_triggers": ["..."],
    "data_confidence": "medium"
  },
  "company": {
    "name": "Acme Corp",
    "industry": "...",
    "location": "...",
    "size": "...",
    "ownership_type": "...",
    "overview": "...",
    "business_model": "..."
  },
  "signals": [
    { "type": "trigger", "description": "...", "source_url": "https://...", "date": null, "relevance": null },
    { "type": "initiative", "description": "...", "source_url": "https://...", "date": null, "relevance": "high" },
    { "type": "pain_point", "description": "...", "source_url": null, "date": null, "relevance": "medium" }
  ],
  "leadership_context": [
    { "name": "...", "title": "...", "relevance_to_prospect": "..." }
  ],
  "unresolved_roles": [
    { "title": "...", "why_likely": "...", "how_to_identify": "..." }
  ],
  "incumbent_vendors": [
    { "name": "...", "category": "...", "relationship": "current user", "displacement_signal": null, "context_note": null }
  ],
  "confidence": "medium",
  "confidence_reasoning": "...",
  "generated_at": "2026-04-29T12:00:00.000Z",
  "sources": ["https://..."],
  "brief_id": "b4e4b0b6-d617-448a-8137-7504e1d7b1a1",
  "next_actions_available": [
    "get_recommended_angle",
    "explain_confidence",
    "suggest_adjacent_research",
    "cite_source"
  ]
}

contact_insights is null when no person-level data could be produced. The prospect has no discoverable public footprint in that case. incumbent_vendors, leadership_context, and unresolved_roles may be empty arrays. brief_id is required for every follow-up call.

GET /api/v1/research/{id}/angles

Returns the recommended sales angle. Built deterministically from the stored brief. No LLM call. Idempotent and safe to cache client-side.

Response:

JSON
{
  "angle": [
    {
      "point": "Lead with how Motive's fleet management platform maps to Jane's priority around \"fleet visibility expansion\" — they own a related priority — \"reducing time-to-incident\".",
      "basis_type": "sourced",
      "basis": "Q3 earnings call mentioned the fleet visibility initiative as a 2026 priority.",
      "source_url": "https://acme.com/investor-relations/q3-2025"
    }
  ],
  "avoid": [
    {
      "point": "Don't lead with displacing Samsara — Jane has a public association with Samsara, so a flanking or coexistence angle will land better than head-to-head displacement.",
      "basis_type": "sourced",
      "basis": "Jane's person brief mentions Samsara: \"...\"",
      "source_url": "https://..."
    }
  ],
  "opening_question": {
    "question": "Given the fleet visibility expansion announcement, what does that mean for reducing time-to-incident?",
    "point": "Given the fleet visibility expansion announcement, what does that mean for reducing time-to-incident?",
    "basis_type": "sourced",
    "basis": "Combines a recent trigger with Jane's stated priority around reducing time-to-incident.",
    "source_url": "https://..."
  },
  "seller_context_used": true,
  "brief_id": "b4e4b0b6-d617-448a-8137-7504e1d7b1a1"
}

basis_type is "sourced"when a matching URL was found in the brief's claim-source map. Otherwise it is "inferred". seller_context_used is falsewhen the user's Sales Context profile is empty. Output falls back to product-agnostic phrasing in that case.

GET /api/v1/research/{id}/confidence

Returns the brief's current confidence level and why.

Response:

JSON
{
  "current_confidence": "medium",
  "limiting_factors": [],
  "what_would_raise_it": []
}

The limiting_factors and what_would_raise_it arrays are scaffolded but currently empty pending final product input. The current_confidence value is the only field that should be relied upon today.

GET /api/v1/research/{id}/adjacent

Re-ranks three categories of adjacent candidates already present in the brief: named leadership beyond the primary prospect, incumbent vendors as displacement targets, and initiatives worth deeper study. Does not fetch new information.Surfaces what's already in the brief, ranked. Business competitors are intentionally excluded. They live in the brief's signals array under their own treatment.

Query parameters:

ParamTypeRequiredNotes
purposeenumnoOne of account_plan, meeting_prep, competitive_landscape. Shapes the ranking.
contextstringnoPer-call intent. Max 2000 chars. Distinctive keywords boost matching candidates. Composes with the seller's Sales Context profile, doesn't replace it.

Response:

JSON
{
  "purpose": "account_plan",
  "context_received": null,
  "candidates": [
    {
      "target_type": "person",
      "target": "John Smith, CFO at Acme Corp",
      "relevance": "high",
      "rationale": "Matches the seller's typical buyer profile (finance); fills a buying-committee gap left by the primary prospect; typically controls budget or vendor selection."
    },
    {
      "target_type": "vendor",
      "target": "Samsara (fleet telematics)",
      "relevance": "high",
      "rationale": "Currently evaluating — window is open; active displacement signal: contract renewal Q1 2026."
    },
    {
      "target_type": "topic",
      "target": "Acme Corp: 2026 fleet visibility expansion",
      "relevance": "high",
      "rationale": "Tied to 2 pain points — high commercial relevance; referenced in 1 recent trigger.",
      "source_url": "https://acme.com/news/q3-earnings"
    }
  ]
}

relevance is low | medium | high. Candidates within each target_type are sorted by score descending. source_url is present on topic candidates. For person and vendor candidates, follow up with research_prospect directly.

POST /api/v1/research/{id}/cite

Returns the source for a specific claim using token-overlap fuzzy matching against the brief's capture-time claim → source map. You don't need to reproduce exact strings. Close paraphrases will match.

Request body:

JSON
{
  "claim": "Q3 revenue grew 40% year over year"
}

Response when matched:

JSON
{
  "matched": true,
  "claim_matched": "Q3 revenue increased 40% YoY versus Q3 2024",
  "url": "https://acme.com/investor-relations/q3-2025",
  "excerpt": "Q3 revenue of $X billion, up 40% year over year...",
  "date": "2025-10-15",
  "support_score": 0.78,
  "support_score_explanation": "Token-overlap score between the queried claim and the stored claim key. 1.0 = near-perfect match; 0.0 = no shared terms. Treat anything under ~0.4 as weakly supported."
}

Response when no match:

JSON
{
  "matched": false,
  "reason": "no_claim_sources_captured",
  "message": "This brief was generated before the claim_sources map was populated, or the research prompt returned an empty map.",
  "support_score": 0
}

matched: falseis a 200 OK response, not an error. It's a successful lookup that returned no match. Treat support_score < 0.4 as weakly supported.

Streaming

SSE format.

The POST /api/v1/research endpoint streams its response as Server-Sent Events because the underlying LLM call can run 60 to 120 seconds, which exceeds typical HTTP gateway idle timeouts.

Wire format. While the call runs, the server emits SSE keepalive comments every 10 seconds:

: keepalive

: keepalive

: keepalive

When generation completes, a single data: event carries the full response body:

data: {"cached":false,"prospect":{...},"company":{...},"brief_id":"..."}

HTTP status is always 200 for the SSE response, even when the body contains an error. Inspect the JSON inside the data: frame to determine success vs. failure. Error frames carry the canonical { error, message, status } shape documented below.

Consuming the stream:

bash
curl -N -H "Authorization: Bearer $KEY" \
     -H "Content-Type: application/json" \
     -d '{"name":"Jane Doe","company":"Acme Corp"}' \
     https://app.repscale.ai/api/v1/research

The -N flag turns off curl buffering. Without it curlmay hold the response until EOF and you won't see keepalives.

In Node, use eventsource or plain fetch + a streaming parser. In Python, use httpx with stream=True and split on \n\n. The format is standard SSE. Any client library that handles text/event-stream works.

The four follow-up endpoints /angles, /confidence, /adjacent, and /citereturn regular JSON, not SSE. They're fast, typically under one second, and don't need streaming.

Errors

Error shape.

Every error response uses the same shape:

JSON
{
  "error": "snake_case_error_code",
  "message": "Human-readable explanation.",
  "status": 400
}

The statusfield mirrors the HTTP status code so consumers reading SSE frames don't need to inspect headers separately.

Error codes

CodeHTTPMeaning
unauthorized401Missing, malformed, revoked, or unrecognized API key. Check the Authorization header.
plan_required403Valid key on a Free plan. REST API requires Pro or Enterprise.
not_found404The referenced brief_id doesn't exist for this key, or has expired. Briefs are retained for 30 days. The message field distinguishes the two.
invalid_request400Malformed JSON body, missing required field, invalid UUID, value out of bounds, or query param failed validation. The message field names the offending fields.
rate_limited429Daily research cap or per-minute follow-up cap exceeded. Inspect X-RateLimit-Reset for when the window opens.
internal_error500Unexpected server-side failure. Retry once. If it persists, contact support with the value from the X-Request-Id response header.

Rate limits

Rate limits.

Limits are enforced per API key. Every response, success and error, carries three headers:

HTTP
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 197
X-RateLimit-Reset: 1714435200

X-RateLimit-Reset is a Unix timestamp in seconds for when the current window resets. Two different reset semantics apply depending on the endpoint:

  • POST /api/v1/research. Daily cap. Reset is the next midnight UTC. The counter rolls over at exactly that moment.
  • All other endpoints. 60-second rolling window. Reset is the end of the current window, when the in-flight bucket expires. Could be anywhere from 0 to 60 seconds out depending on when the window started.

Both are valid uses of the X-RateLimit-Reset header per the IETF draft. The mixed semantics reflect the difference between "we metered this because it costs LLM tokens" and "we metered this because it could be called in a tight loop".

Caps by plan

Plan/api/v1/research per dayAll other endpoints (per minute)
Freen/a, 403 plan_requiredn/a
Pro200120
Enterprise1,000120

The Pro 200/day is a safety ceiling for runaway agent loops, not a plan restriction. Contact support to raise it for production workloads.

The 200/day Pro cap is shared across the web app, MCP, and REST. Burning 50 briefs in the web app leaves 150 for API consumers.

When a cap is exceeded, the response is 429 rate_limited with X-RateLimit-Remaining: 0 and X-RateLimit-Reset set to the next window boundary.

CORS

Cross-origin requests.

All /api/v1/ endpoints accept cross-origin requests from any origin via Access-Control-Allow-Origin: *. API keys are required for every authenticated call regardless of origin, so an unauthorized origin can't do anything anyway. Origin allow-listing is on the roadmap for enterprise customers who need it. Contact support if relevant.

OPTIONS preflight requests return 204 with the standard CORS header block. Allowed methods are GET, POST, OPTIONS. Allowed headers are Authorization, Content-Type, x-api-key. Rate-limit and request-id headers are exposed via Access-Control-Expose-Headersso they're readable from browser code.

Quick start

Three calls, one prospect.

1. Generate a brief

bash
KEY="rsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

curl -N -H "Authorization: Bearer $KEY" \
     -H "Content-Type: application/json" \
     -d '{"name":"Jane Doe","company":"Acme Corp","title":"VP of Operations"}' \
     https://app.repscale.ai/api/v1/research

Output is an SSE stream. Watch for the final data:line. That's the JSON. Pull brief_idout of it. You'll need it for the next two calls.

2. Get the recommended angle

bash
ID="b4e4b0b6-d617-448a-8137-7504e1d7b1a1"  # from step 1

curl -H "Authorization: Bearer $KEY" \
     "https://app.repscale.ai/api/v1/research/$ID/angles"

3. Verify a claim

bash
curl -H "Authorization: Bearer $KEY" \
     -H "Content-Type: application/json" \
     -d '{"claim":"Q3 revenue grew 40% year over year"}' \
     "https://app.repscale.ai/api/v1/research/$ID/cite"

That's the full loop: research → angle → cite. The other two endpoints, /confidence and /adjacent, follow the same pattern as /angles.

OpenAPI spec

Machine-readable spec.

A machine-readable OpenAPI 3.1 spec is served at https://app.repscale.ai/api/v1/openapi. It requires no authentication. Use it to generate clients with openapi-generator or to render Swagger UI / ReDoc.

Support

Get help.

  • API key issues, rate-limit raises, enterprise origin allow-listing: info@repscale.ai
  • Bug reports: include the X-Request-Id from the response headers and the timestamp of the failing call.
  • Schema changes are versioned. The v1 contract on this page is frozen. When v2 ships, this page stays valid for v1 consumers indefinitely.