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.app/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 errorsheaders 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.