API

The programmatic API is the same scoring path the Manual Spectrometer uses in the browser — one bearer key, one endpoint to score a specimen, and a second to check your account's standing.

Generate a token

From your workspace Account page, the API key card has a Generate API key button. The full key (fsk_…) is shown exactly once — copy it immediately, since only its SHA-256 hash is ever stored server-side. Afterward the card shows just the last four characters (fsk_…a1b2) and its creation date.

Regenerate mints a new key and invalidates the old one immediately — there's no overlap window, so rotate during a maintenance window if you have integrations depending on the current key. Revoke removes the key entirely with nothing to replace it until you generate a new one.

// Keep it server-side

Your API key can spend your account's manual-query quota and read your plan status. Never ship it in client-side code — per Foreshock's terms, a user's own browser can never call the API directly; requests must come from your server and carry your key.

Authentication

Every request carries the key as a bearer token:

curl https://foreshock.io/api/v1/me \
  -H "Authorization: Bearer $FORESHOCK_API_KEY"

Check your account

GET /api/v1/me returns your plan and current manual-query quota — no specimen, no scoring, just status:

{
  "ok": true,
  "plan": "graduated",
  "quota": { "used": 41, "total": 500, "remaining": 459 }
}

Make a request

POST /api/v1/score scores one specimen and spends one unit of your manual-query quota. It accepts either a JSON body or a raw text/plain body:

curl https://foreshock.io/api/v1/score \
  -H "Authorization: Bearer $FORESHOCK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "We absolutely crushed the competition today!", "limit": 20, "elbow": true}'

Recognized JSON fields, all optional besides text:

Field Type What it does
text string the specimen to score
limit number cap the number of rows returned
minSimilarity number drop rows below this similarity
minDot number drop rows below this dot product
elbow boolean truncate the spectrum at its Kneedle knee — see below
stripCode boolean strip code-like tokens before scoring
protectShunts boolean exclude your account's own shunts from the response
embedding boolean also return the raw embedding vector the scorer computed

If the scorer fails outright, the spent quota unit is refunded automatically — a failed request never silently costs you a score.

Interpret the response

{
  "ok": true,
  "receipt": "b6e1c2f0-...",
  "ms": 84,
  "quota": { "used": 42, "total": 500, "remaining": 458 },
  "results": [
    { "key": "++joy", "similarity": 0.71, "distance": 0.58, "dotproduct": 0.71 },
    { "key": "|+action", "similarity": 0.34, "distance": 1.12, "dotproduct": 0.19 },
    { "key": "@Sarcasm", "similarity": 0.06, "distance": 1.61, "dotproduct": 0.02 },
    { "key": "__shunt_contributor_abuse", "similarity": 0.03, "distance": 1.71, "dotproduct": 0.01 }
  ]
}
  • receipt — a UUID you can hand to anyone as a permalink (/client/query/<uuid>) to view the same result without exposing the scored text.
  • ms — the scorer's own round-trip time.
  • results — one row per matched key: similarity and dotproduct run higher-is-closer; distance runs lower-is-closer (it's the standardized distance to that concept's centroid).

Reading a key

A result's key sigil tells you which of the three sensor families it came from:

Prefix / shape Family Example
bare word, +word, ++word graduated valence, three intensity tiers anger, +anger, ++anger
|word signed index (outlook/action) |outlook, |+action
@Word pragmatic saturation scalar @Sarcasm, @Gratitude
__shunt_name / ~~shunt_name a shunt tripwire __shunt_xenophobia
%markov_word baseline comparison %markov_ev

Examining shunts in context

Rows whose key starts with __shunt_ or ~~shunt_ are shunts — your account's Shunt Studio bank, or a repository's .foreshock.yml custom shunts, depending on where the request originated. A shunt appearing in the results doesn't mean it tripped — it means it was scored; whether it counts as a trip depends on the sensitivity threshold configured wherever that shunt lives (see sensitivity thresholds). Pass protectShunts: true to exclude your own account's shunts from a response entirely — useful if you're building something public on top of this endpoint and don't want callers able to fingerprint what your community privately watches for.

What does the Kneedle do?

Set elbow: true to have the scorer auto-truncate the returned spectrum at its knee — the point in the sorted, descending score curve past which everything is floor rather than signal. Without it, you get every row above your similarity/dot floors, however long that list runs. With it, you get only the dimensions Aldous considers meaningfully present. See the Kneedle for the algorithm and a worked example. Signed indexes and pragmatic scalars follow their own rules regardless of this flag — they're either always kept (signed indexes) or always judged against their own 20% floor (scalars).

How rate limits work

Rate limiting is separate from your monthly manual-query quota — the quota is a budget (resets each calendar month, tied to your plan), while the rate limit is a velocity cap (a rolling window, always in force, tied to actual scoring cost). The scarce resource isn't requests, it's CPU on the embedding model, so the limiter counts work units — roughly one unit per specimen embedded — rather than raw request counts. A batch call that embeds several specimens at once costs proportionally more than one unit.

Two windows are checked on every request, and both need room:

  • Per-key — fairness, so one integration can't consume the whole box.
  • Global — capacity, because the ceiling belongs to the embedding server, not to any one caller.

If either window is full, you get a 429 with a Retry-After header (seconds) and a body identifying which window was the blocker:

{
  "ok": false,
  "error": "You're going too fast — retry in 12s.",
  "retryAfter": 12,
  "scope": "user",
  "limit": 20
}

scope: "global" means the service as a whole is at capacity rather than your account specifically — the same Retry-After guidance applies either way.

Webhooks

// Coming soon

Two delivery tiers are planned. Best Effort is delivered in-house at roughly a 97% average success rate, with no delivery guarantee — free on every plan. Guaranteed delivery runs through HookDeck Outpost and is limited to higher tiers. This page will be updated with endpoint and payload details once webhooks ship.

Where to go next

The Manual Spectrometer is the same endpoint with a browser UI in front of it — a good way to see a payload shape before you write code against it. For the math behind what you're reading in a response, see How Aldous scores language and Shunts & Latent Concept Erasure.