API Documentation

Submit content for expert panel evaluation programmatically. Get scores, improvement suggestions, and optimized variants via a simple REST API.

Authentication

All API requests require an API key passed in the Authorization header. Generate API keys from your Settings page.

Authorization Header
Authorization: Bearer rfnd_live_YOUR_API_KEY
Security notes:
  • 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.

Mode10 experts20 experts30 experts
Quick Pulse153045
Expert Review306090
Deep Simulation4590135

Multi-round: multiply by number of rounds. Competitive context adds 50 credits (+25 if auto-generated).

Endpoints

POST/api/v1/evaluate

Submit content for evaluation. Returns a testRunId for polling.

Request Body

JSON
{
  "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

200 OK
{
  "data": {
    "testRunId": "k57abc123def456",
    "status": "queued",
    "creditsCharged": 100,
    "estimatedDuration": "30-90s"
  }
}
GET/api/v1/results/:testRunId

Poll 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 }
}
GET/api/v1/balance

Check your credit balance.

200 OK
{
  "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
HMAC secret: Webhooks are signed with the SHA-256 hash of your API key. To verify, hash your raw API key with SHA-256 and use that as the HMAC secret:apiKeyHash = SHA256("rfnd_live_your_key_here")

Signature Verification (Node.js)

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)

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)
Delivery: 3 attempts with backoff (+30s, +5min). Webhooks must respond within 10 seconds with a 2xx status.

Error Codes

All errors return a consistent JSON format:

{
  "error": {
    "code": "insufficient_credits",
    "message": "Insufficient credits for this evaluation",
    "details": { "required": 300, "balance": 150 }
  }
}
HTTPCodeWhen
400invalid_requestMalformed JSON or missing required fields
401unauthorizedMissing, invalid, or revoked API key
402insufficient_creditsNot enough credits for this evaluation
404not_foundTest run not found or not owned by you
422validation_errorInvalid parameters (contentType, panelSize, etc.)
429too_many_requestsMore than 100 concurrent evaluations
500internal_errorUnexpected server error

Full Example (Python)

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)