Partner API
v1 · stable · JSON in / JSON out · per-key auth

Programmatic scoring for partners.

One POST, one JSON scorecard. The same deterministic 10-axis engine that powers the on-site /score form, returned as a structured response your app can render or route without setting up a separate scoring stack.

Get a key

One key per integration. Plaintext shown once.

We provision keys by hand on a per-partner basis so we know who's calling the engine. Ask us for one by email — once issued we run the deploy-time seed and your plaintext lands in the server log exactly once, in this format:

Server log (first boot after the key is seeded)

════════════════════════════════════════════════════════════════
  /api/v1/score: seeded first ApiKey (label=partner-default)
  plaintext : hlms_<48 hex chars>
  Copy it now — it will not be shown again. Rotate by setting
  ApiKey.revokedAt = now() on the old row, then deleting it so the
  next seed run generates a fresh plaintext.
════════════════════════════════════════════════════════════════

Rotate by setting revokedAt on the current row (the endpoint will start returning 401), then deleting the row so the next deploy re-seeds with a fresh plaintext.

Request

One endpoint. POST only.

POST /api/v1/score with a JSON body of exactly { "input": "…" }. Unknown keys (including email) are rejected — the schema is strict.

Sample request

curl --request POST 'https://helmslee.polsia.io/api/v1/score' \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: hlms_<your-key-here>' \
  --data '{ "input": "A vertical SaaS for independent venue owners running weekly open mics — scheduling, payouts, and a public lineup page." }'
MethodPOST
Path/api/v1/score
Headerx-api-key: <plaintext>
Body{ "input": "string ≤2000 chars" }
Timeoutno client-side cap; engine <1s p99
Idempotencyno state; repeated POSTs return equivalent scores

Response

One composite score, ten axes, four winnability signals.

score is 0–100. breakdown.axes is the same ten-axis scorecard as on-site. recommendations surfaces the strongest "improve this" signals only. No idea text, no slug, no timestamp, no email — the response is safe to log and cache.

Sample response (200)

{
  "score": 62,
  "breakdown": {
    "axes": [
      {
        "key": "demand",
        "label": "Demand",
        "score": 8,
        "verdict": "pass",
        "rationale": "Search interest has held for 12+ months or names a clear buyer.",
        "sourceUrl": "https://trends.google.com/"
      },
      {
        "key": "competition",
        "label": "Competition",
        "score": 6,
        "verdict": "neutral",
        "rationale": "Existing tools validate the category, but the wedge is still specific.",
        "sourceUrl": "https://www.g2.com/"
      },
      {
        "key": "profitability",
        "label": "Profitability",
        "score": 7,
        "verdict": "pass",
        "rationale": "A recurring workflow creates a clear path to paid operational value.",
        "sourceUrl": "https://www.bls.gov/ooh/"
      },
      {
        "key": "scalability",
        "label": "Scalability",
        "score": 8,
        "verdict": "pass",
        "rationale": "Software can serve more venues without matching growth in labor.",
        "sourceUrl": "https://www.census.gov/programs-surveys/economic-census.html"
      },
      {
        "key": "automation",
        "label": "Automation",
        "score": 7,
        "verdict": "pass",
        "rationale": "Scheduling and payout coordination are repeatable, rules-based tasks.",
        "sourceUrl": "https://www.nist.gov/"
      },
      {
        "key": "retention",
        "label": "Retention",
        "score": 6,
        "verdict": "neutral",
        "rationale": "Weekly operating workflows can retain users if the lineup becomes habitual.",
        "sourceUrl": "https://www.sba.gov/business-guide/manage-your-business"
      },
      {
        "key": "seo",
        "label": "SEO surface",
        "score": 5,
        "verdict": "neutral",
        "rationale": "Venue and event pages create useful local search surfaces over time.",
        "sourceUrl": "https://developers.google.com/search/docs"
      },
      {
        "key": "virality",
        "label": "Virality",
        "score": 4,
        "verdict": "fail",
        "rationale": "The workflow is shareable, but each venue may not invite many new users.",
        "sourceUrl": "https://www.pewresearch.org/internet/"
      },
      {
        "key": "startupCost",
        "label": "Startup cost",
        "score": 7,
        "verdict": "pass",
        "rationale": "The first version can launch with standard scheduling and payment tooling.",
        "sourceUrl": "https://www.sba.gov/business-guide/plan-your-business"
      },
      {
        "key": "technicalDifficulty",
        "label": "Technical difficulty",
        "score": 6,
        "verdict": "neutral",
        "rationale": "Payments and scheduling add edge cases, but the core model is conventional.",
        "sourceUrl": "https://developer.mozilla.org/en-US/docs/Web/API"
      }
    ],
    "winnabilityScore": 64,
    "winnabilityBreakdown": {
      "signals": [
        {
          "key": "incumbentStrength",
          "label": "Incumbent strength",
          "score": 6,
          "verdict": "neutral"
        },
        {
          "key": "switchingCost",
          "label": "Switching cost",
          "score": 5,
          "verdict": "neutral"
        },
        {
          "key": "asymmetricAdvantage",
          "label": "Asymmetric advantage",
          "score": 7,
          "verdict": "pass"
        },
        {
          "key": "structuralLiability",
          "label": "Structural liability",
          "score": 6,
          "verdict": "neutral"
        }
      ],
      "score": 64,
      "verdict": "neutral"
    }
  },
  "recommendations": [
    {
      "axis": "virality",
      "verdict": "fail",
      "message": "Improve `Virality` — this axis is below the pass bar."
    },
    {
      "axis": "winnability.switchingCost",
      "verdict": "neutral",
      "message": "Sharpen `Switching cost` — score 5/10."
    }
  ]
}
scoreinteger 0–100 — rounded composite
breakdown.axes10 entries, fixed order
breakdown.winnabilityScoreinteger 0–100 — sibling metric
breakdown.winnabilityBreakdown.signals4 entries: incumbentStrength, switchingCost, asymmetricAdvantage, structuralLiability
recommendationsflat list of {axis, verdict, message}; ≤160-char `message`
verdict'pass' | 'neutral' | 'fail'

Rate limit

60 requests per key per minute.

Sliding-window, in-process for the first slice (easy to swap for Redis later — the public signature in src/lib/api/rate-limit.ts is the swap point). Two headers surface on every 200 response:

X-RateLimit-Remaininginteger — slots left in the current window
X-RateLimit-Resetunix seconds — when the oldest slot in the window frees

On exhaustion we return 429 with Retry-After (seconds) and the same reset header. The limit is per-key, not per-IP, so partners behind NAT share one budget — issue more keys if you need more parallelism.

Errors

400. 401. 429. That's it.

Every error response is JSON. Look at error or errors — headers carry the reset time on 429.

Bad input
400

When

Body is not JSON, `input` is missing/empty, or unknown keys are present.

Fix

Send exactly `{ "input": "your idea" }`. The endpoint rejects any other field.

Missing or invalid key
401

When

No `x-api-key` header, header is empty, the digest does not match a row, or the row is revoked.

Fix

Re-check the header. If the key was revoked, request a new one — old plaintexts are not re-derivable.

No or empty `x-api-key`

{ "error": "missing api key" }

Unknown or revoked key

{ "error": "invalid or revoked api key" }
Rate limit exceeded
429

When

More than 60 requests in a 60-second window for the same key.

Fix

Wait for `Retry-After` seconds, then resume. The budget is per-key, so spread across keys if you need more throughput.

Need a key or a quota bump?

Email us. We provision by hand.

We track every integration on a per-partner basis so abuse is contained and we know what wedges the engine is hitting. Drop us a line with the volume you expect and a description of the surface you'll be wiring into.