API Documentation
Everything you need to integrate Candor's deception detection into your application. Start with a free API key — no credit card required.
Overview
Candor exposes a single REST endpoint: POST /api/analyze.
Send text, get back a deception score (0–100), a verdict, five linguistic signal breakdowns,
and a list of flagged sentences — all in one call.
The API uses linguistic analysis grounded in peer-reviewed deception research (Newman 2003, DePaulo 2003, Pennebaker 2011). It's designed for programmatic use: fraud detection pipelines, content integrity systems, legal analysis tools, and anywhere reliable language analysis adds value.
Base URL
https://getcandor.polsia.app
Request format
All requests are JSON over HTTPS. Set Content-Type: application/json on every request.
Authentication
Every request needs your API key. You get one automatically when you create a free account at getcandor.polsia.app/auth. API keys look like this:
ck_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
Pass your key in the X-API-Key request header on every call:
X-API-Key: ck_your_api_key_here
What happens without a key?
Requests without a valid X-API-Key header return a 401 Unauthorized error.
The API doesn't allow anonymous calls — all usage is tracked to your account for billing purposes.
Your First Request
Go to getcandor.polsia.app/auth and sign up. Your API key is generated immediately.
You'll see your key on the dashboard, formatted as ck_.... Copy it.
Replace YOUR_API_KEY below and run this in your terminal:
curl -X POST https://getcandor.polsia.app/api/analyze \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"text": "I definitely did not take the money. I was nowhere near the office that day."}'
You'll get back a JSON object with a score, verdict, signals, and flagged sentences. See Response Format for a full breakdown.
POST /api/analyze
Analyzes a text string for deception signals and returns a structured report.
Request body
Example requests
curl -X POST https://getcandor.polsia.app/api/analyze \ -H "Content-Type: application/json" \ -H "X-API-Key: ck_your_api_key_here" \ -d '{"text": "The transaction was entirely legitimate and authorized by all parties involved."}'
import requests api_key = "ck_your_api_key_here" text = "The transaction was entirely legitimate and authorized by all parties involved." response = requests.post( "https://getcandor.polsia.app/api/analyze", headers={ "Content-Type": "application/json", "X-API-Key": api_key, }, json={"text": text} ) data = response.json() if data["success"]: analysis = data["analysis"] print(f"Score: {analysis['score']}/100") print(f"Verdict: {analysis['verdict']}") print(f"Deceptive: {analysis['is_deceptive']}") else: print(f"Error: {data['message']}")
const response = await fetch("https://getcandor.polsia.app/api/analyze", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "ck_your_api_key_here", }, body: JSON.stringify({ text: "The transaction was entirely legitimate and authorized by all parties involved.", }), }); const data = await response.json(); if (data.success) { const { score, verdict, is_deceptive, signals } = data.analysis; console.log(`Score: ${score}/100 — ${verdict}`); console.log(`Deceptive: ${is_deceptive}`); } else { console.error(`Error: ${data.message}`); }
Response Format
A successful response always has "success": true and an analysis object.
{
"success": true,
"analysis": {
"score": 67,
"confidence": 0.85,
"verdict": "high_risk",
"is_deceptive": true,
"signals": {
"pronoun_distancing": {
"score": 72,
"finding": "Low first-person pronoun usage suggests psychological distancing from the claims."
},
"hedging": {
"score": 48,
"finding": "Moderate use of tentative language reduces commitment to stated facts."
},
"detail_specificity": {
"score": 61,
"finding": "Vague temporal and spatial references; lacks sensory detail expected in truthful recall."
},
"cognitive_complexity": {
"score": 71,
"finding": "Simple sentence structures may indicate reduced cognitive load from not constructing truthful narrative."
},
"emotional_leakage": {
"score": 69,
"finding": "Incongruent emotional tone — over-emphasis on legitimacy without organic elaboration."
}
},
"flagged_sentences": [
{
"text": "The transaction was entirely legitimate and authorized by all parties involved.",
"reason": "Over-assertion of legitimacy without supporting context; scripted-sounding.",
"severity": "high"
}
],
"summary": "The text shows multiple deception indicators across linguistic dimensions."
},
"meta": {
"input_length": 79,
"processing_time_ms": 2847,
"model": "candor-v1",
"tier": "free",
"usage_remaining": 47
}
}
Field reference
low_risk (0–25),
moderate_risk (26–41),
high_risk (42–75),
very_high_risk (76–100).
true when score ≥ 30. Recalibrated against a 227-sample LIAR political-speech corpus (precision=56%, recall=51%, F1=0.534). Use this for simple pass/fail decisions.
finding for each.
See The 5 Signals.
text, reason, and severity
("low", "medium", "high").
May be empty if no individual sentences stand out.
The 5 Signals
Each signal maps to a dimension identified in peer-reviewed deception research.
Scores are 0–100 per signal; the overall score is a weighted composite.
| Signal | Key field | What it measures |
|---|---|---|
| Pronoun Distancing | pronoun_distancing |
Reduced use of "I", "me", "my" signals psychological distancing from statements. Liars avoid ownership of their claims. (Newman 2003, Pennebaker 2011) |
| Hedging & Uncertainty | hedging |
Tentative language ("maybe", "perhaps", "sort of", "I think") signals lack of commitment to claims — the writer leaving room to retreat. |
| Detail Specificity | detail_specificity |
Truthful accounts contain sensory detail, temporal markers, spatial references. Deceptive accounts tend to be vague or suspiciously scripted. (DePaulo 2003) |
| Cognitive Complexity | cognitive_complexity |
Lying requires more mental effort, producing detectable linguistic artifacts: simpler structures, fewer embedded clauses. (Vrij et al. 2010) |
| Emotional Leakage | emotional_leakage |
Incongruent emotional tone — emotions that don't fit the context, over-assertion without organic elaboration. (Hancock et al. 2004) |
Error Codes
All errors return JSON with "success": false and a human-readable message.
| Code | Meaning | How to fix it |
|---|---|---|
| 400 | Bad Request |
Missing text field, text too short (<10 chars), or text too long (>10,000 chars).
Check your request body.
|
| 401 | Unauthorized |
Missing or invalid X-API-Key header. Double-check you're sending the header
and that the key matches exactly what's shown in your dashboard.
|
| 402 | Limit Reached | You've used your monthly analysis quota. Limits reset on the 1st of every month. Upgrade your plan for more. |
| 429 | Rate Limited | Too many requests. Free plan allows 20 requests/minute. Wait 60 seconds, then retry. Add exponential backoff for production systems. |
| 500 | Server Error | Analysis failed on our end. Retry once — these are usually transient. If it persists, check our status or contact support. |
Error response shape
{
"success": false,
"message": "Monthly limit reached. Upgrade to continue."
}
Rate Limits
Rate limits are per API key, applied on a rolling 60-second window.
| Plan | Requests / minute | Notes |
|---|---|---|
| Free | 20 req/min | Sufficient for testing and low-volume integrations. |
| Pro | 60 req/min | Suitable for most production workloads. |
| Enterprise | Custom | Contact us to discuss custom limits for high-volume pipelines. |
Handling 429s in code
Use exponential backoff when you receive a 429. Here's a simple pattern:
import time, requests def analyze_with_retry(text, api_key, max_retries=3): for attempt in range(max_retries): response = requests.post( "https://getcandor.polsia.app/api/analyze", headers={"X-API-Key": api_key}, json={"text": text} ) if response.status_code == 429: wait = 2 ** attempt # 1s, 2s, 4s time.sleep(wait) continue return response.json() raise Exception("Rate limit exceeded after retries")
Plans & Limits
Monthly analysis quotas reset on the 1st of each month. View full pricing →
Troubleshooting
Getting a 401 even with the right key?
Check that you're using the header name X-API-Key (not Authorization).
Also confirm there are no extra spaces before or after the key value.
Copy directly from your dashboard to avoid typos.
Score seems too high / too low?
The model is calibrated so that most normal text scores 10–25. If you're getting
unexpectedly high scores on text you believe is truthful, consider whether the text
is highly formal, legalistic, or scripted — those styles can trigger some signals.
Very short texts (<20 words) also tend to produce noisier scores; check the
confidence field.
Response is slow?
The first request after a period of inactivity may take 5–15 seconds (cold start). Subsequent requests typically return in 2–4 seconds. If you consistently see latency over 10 seconds, contact support.
No flagged_sentences in the response?
This is normal. The model only flags sentences with genuine, specific deception markers.
A moderate overall score can occur from subtle signals spread across the text
without any single sentence standing out. Check the signals object for
which dimensions drove the score.
Request body appears empty?
Make sure you're setting the Content-Type: application/json header
and that your JSON is valid. If using curl, wrap the JSON in single quotes
(not double) to avoid shell interpolation issues.