Authentication
All API requests require an API key passed in the Authorization header. Generate API keys from your Settings page.
Authorization: Bearer rfnd_live_YOUR_API_KEY
- API keys are shown once at creation — save them securely
- Only pass keys via the Authorization header, never in URLs
- Revoked keys are rejected immediately
Credit Costs
Credits are charged upfront when you submit an evaluation. If the evaluation fails, credits are automatically refunded.
| Mode | 10 experts | 20 experts | 30 experts |
|---|---|---|---|
| Quick Pulse | 15 | 30 | 45 |
| Expert Review | 30 | 60 | 90 |
| Deep Simulation | 45 | 90 | 135 |
Multi-round: multiply by number of rounds. Competitive context adds 50 credits (+25 if auto-generated).
Endpoints
/api/v1/evaluateSubmit content for evaluation. Returns a testRunId for polling.
Request Body
{
"content": "10 Morning Habits That Changed My Life",
"contentType": "youtube",
"mode": "expert_review", // optional, default: "expert_review"
"panelSize": 20, // optional, default: 20 (10, 20, or 30)
"rounds": 1, // optional, 1-5, default: 1
"targetScore": 8.0, // optional, 1-10 — early-stop threshold
"context": { // optional
"targetAudience": "25-35 year old professionals",
"niche": "productivity",
"tone": "authentic",
"goal": "maximize CTR"
},
"webhookUrl": "https://...", // optional, HTTPS only
"competitiveContext": { // optional
"enabled": true,
"competitors": ["Competitor title 1"],
"autoGenerated": false
}
}Response
{
"data": {
"testRunId": "k57abc123def456",
"status": "queued",
"creditsCharged": 100,
"estimatedDuration": "30-90s"
}
}/api/v1/results/:testRunIdPoll for evaluation results. Returns progress while evaluating, full results when complete.
While evaluating
{
"data": {
"testRunId": "k57abc123def456",
"status": "evaluating",
"progress": { "completed": 12, "total": 20 }
}
}When complete
{
"data": {
"testRunId": "k57abc123def456",
"status": "complete",
"content": { "text": "...", "type": "youtube" },
"scores": {
"overall": 6.8,
"clickRate": 0.55,
"dimensions": [
{ "name": "Dimension 1", "score": 7.2 },
{ "name": "Dimension 2", "score": 6.5 }
],
"feedDistinctiveness": 5.3,
"regulatoryFit": 7.1
},
"suggestions": [{
"priority": 1,
"dimension": "Area of improvement",
"suggestion": "Specific actionable advice...",
"rationale": "Why this matters for your content..."
}],
"improvedVariants": [{
"text": "An optimized version of your content...",
"rank": 1,
"predictedScore": 8.2,
"principles": ["Principle 1", "Principle 2"]
}],
"panelSize": 20,
"mode": "expert_review",
"creditsCharged": 100,
"completedAt": 1711324800000
}
}Multi-round (iterations)
When rounds > 1, the response includes an iterations object with the full improvement chain.
"iterations": {
"totalRounds": 3,
"completedRounds": 2,
"bestRound": 2,
"earlyStopped": true,
"chain": [
{ "round": 1, "content": "...", "overall": 6.8, "clickRate": 0.55 },
{ "round": 2, "content": "...", "overall": 8.6, "clickRate": 0.77 }
],
"bestContent": "I Tried 10 Morning Habits...",
"bestScore": 8.6,
"bestClickRate": 0.77,
"improvement": { "scoreChange": 1.8, "clickRateChange": 0.22 }
}/api/v1/balanceCheck your credit balance.
{
"data": {
"credits": 1500
}
}Webhooks
Instead of polling, provide a webhookUrl when submitting an evaluation. Refynd will POST results to your URL when complete.
Headers
X-Refynd-Signature: sha256=<hex> X-Refynd-Timestamp: <unix_ms> X-Refynd-Event: evaluation.complete
apiKeyHash = SHA256("rfnd_live_your_key_here")Signature Verification (Node.js)
const crypto = require("crypto");
// Compute this once from your raw API key:
// const apiKeyHash = crypto.createHash("sha256")
// .update("rfnd_live_your_key").digest("hex");
function verifyWebhook(body, signature, timestamp, apiKeyHash) {
// Reject if timestamp is more than 5 minutes old
if (Date.now() - Number(timestamp) > 5 * 60 * 1000) {
throw new Error("Webhook timestamp too old");
}
const expected = crypto
.createHmac("sha256", apiKeyHash)
.update(body)
.digest("hex");
const received = signature.replace("sha256=", "");
if (!crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(received)
)) {
throw new Error("Invalid webhook signature");
}
return JSON.parse(body);
}Signature Verification (Python)
import hmac, hashlib, json, time
# Compute once from your raw API key:
# api_key_hash = hashlib.sha256(b"rfnd_live_your_key").hexdigest()
def verify_webhook(body: bytes, signature: str,
timestamp: str, api_key_hash: str) -> dict:
if time.time() * 1000 - int(timestamp) > 5 * 60 * 1000:
raise ValueError("Webhook timestamp too old")
expected = hmac.HMAC(
api_key_hash.encode(), body, hashlib.sha256
).hexdigest()
received = signature.replace("sha256=", "")
if not hmac.compare_digest(expected, received):
raise ValueError("Invalid webhook signature")
return json.loads(body)Error Codes
All errors return a consistent JSON format:
{
"error": {
"code": "insufficient_credits",
"message": "Insufficient credits for this evaluation",
"details": { "required": 300, "balance": 150 }
}
}| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | Malformed JSON or missing required fields |
| 401 | unauthorized | Missing, invalid, or revoked API key |
| 402 | insufficient_credits | Not enough credits for this evaluation |
| 404 | not_found | Test run not found or not owned by you |
| 422 | validation_error | Invalid parameters (contentType, panelSize, etc.) |
| 429 | too_many_requests | More than 100 concurrent evaluations |
| 500 | internal_error | Unexpected server error |
Full Example (Python)
import requests, time
API_KEY = "rfnd_live_YOUR_KEY"
BASE = "https://refynd.ca/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# 1. Submit evaluation
resp = requests.post(f"{BASE}/evaluate", json={
"content": "10 Morning Habits That Changed My Life",
"contentType": "youtube",
"mode": "expert_review",
"panelSize": 20,
}, headers=HEADERS)
test_run_id = resp.json()["data"]["testRunId"]
# 2. Poll for results
while True:
result = requests.get(
f"{BASE}/results/{test_run_id}", headers=HEADERS
).json()["data"]
if result["status"] == "complete":
print(f"Score: {result['scores']['overall']}")
for v in result["improvedVariants"]:
print(f" Variant: {v['text']} (predicted: {v['predictedScore']})")
break
elif result["status"] == "failed":
print("Evaluation failed — credits refunded")
break
print(f"Progress: {result['progress']['completed']}/{result['progress']['total']}")
time.sleep(5)