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 keySetup
Base URL.
https://repscale.aiAll 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:
Authorization: Bearer rsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-api-key: rsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGetting a key
- Sign in at repscale.ai
- Go to Settings → API Keys
- Generate a key and copy the plaintext. It's shown exactly once.
Revoke from the same page. Revocation takes effect immediately.
Auth failures
| Condition | Status | error code |
|---|---|---|
Missing or malformed Authorization header | 401 | unauthorized |
| Key not found, revoked, or for a deleted user | 401 | unauthorized |
| Valid key on a Free plan | 403 | plan_required |
Reference
Endpoints.
All five endpoints are versioned under /api/v1/.
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/research | Generate a research brief. SSE-streamed. |
| GET | /api/v1/research/{id}/angles | Recommended sales angle, what-to-avoid list, opening question. |
| GET | /api/v1/research/{id}/confidence | Why the brief is low/medium/high confidence. |
| GET | /api/v1/research/{id}/adjacent | Ranked adjacent candidates: other execs, vendors, initiatives. |
| POST | /api/v1/research/{id}/cite | Source URL + excerpt + date for a specific claim. |
| GET | /api/v1/openapi | OpenAPI 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:
{
"name": "Jane Doe",
"company": "Acme Corp",
"title": "VP of Operations",
"website": "https://acme.com",
"context": "Met at SaaStr 2025; mentioned fleet visibility pain."
}| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | Full name of the prospect. Max 200 chars. |
| company | string | yes | Company name. Max 300 chars. |
| title | string | no | Job title. Improves the brief. |
| website | string | no | Company URL. Used as the primary first-party source. |
| context | string | no | Seed context: call notes, LinkedIn bio. Max 5000 chars. Treated as input, not echoed back. |
Response body. The JSON inside the final data: SSE frame:
{
"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:
{
"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:
{
"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:
| Param | Type | Required | Notes |
|---|---|---|---|
| purpose | enum | no | One of account_plan, meeting_prep, competitive_landscape. Shapes the ranking. |
| context | string | no | Per-call intent. Max 2000 chars. Distinctive keywords boost matching candidates. Composes with the seller's Sales Context profile, doesn't replace it. |
Response:
{
"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:
{
"claim": "Q3 revenue grew 40% year over year"
}Response when matched:
{
"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:
{
"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
: keepaliveWhen 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:
curl -N -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","company":"Acme Corp"}' \
https://app.repscale.ai/api/v1/researchThe -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:
{
"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
| Code | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing, malformed, revoked, or unrecognized API key. Check the Authorization header. |
| plan_required | 403 | Valid key on a Free plan. REST API requires Pro or Enterprise. |
| not_found | 404 | The 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_request | 400 | Malformed JSON body, missing required field, invalid UUID, value out of bounds, or query param failed validation. The message field names the offending fields. |
| rate_limited | 429 | Daily research cap or per-minute follow-up cap exceeded. Inspect X-RateLimit-Reset for when the window opens. |
| internal_error | 500 | Unexpected 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:
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 197
X-RateLimit-Reset: 1714435200X-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 day | All other endpoints (per minute) |
|---|---|---|
| Free | n/a, 403 plan_required | n/a |
| Pro | 200 | 120 |
| Enterprise | 1,000 | 120 |
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
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/researchOutput 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
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
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-Idfrom the response headers and the timestamp of the failing call. - Schema changes are versioned. The
v1contract on this page is frozen. Whenv2ships, this page stays valid forv1consumers indefinitely.