Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
Bring your own backend
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
Entitlement gates
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Gateway deny wire-codes (FSDENYCODES)Retryable codesUpgrade affordance — limitCodeRuntime verification statusesHTTP status taxonomyHow to debug a denial
Add a metered capability
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Reference/Response & deny codes

Response & deny codes

HTTP statuses and the canonical gateway deny wire-codes.

PreviousEnvironment variablesNextAdd a metered capability

When the gateway denies a request it returns a flat JSON body: a human-readable error plus a machine-readable code. Branch on the code — the message can change between releases, the code can't. The frontend SDK exposes the same vocabulary as FS_DENY_CODES on @farthershore/farthershore-js, so client logic and support workflows share one source of truth.

{ "error": "Rate limit reached for this key.", "code": "rate_limited" }
import { FartherShoreApiError, FS_DENY_CODES } from "@farthershore/farthershore-js";

try {
  await fs.feature("cron-jobs").json("/v1/cron-jobs");
} catch (err) {
  if (err instanceof FartherShoreApiError) {
    switch (err.code) {
      case FS_DENY_CODES.rate_limited:    return backOffAndRetry();
      case FS_DENY_CODES.credit_exhausted: return promptTopUp();
      case FS_DENY_CODES.feature_not_enabled: return promptUpgrade();
    }
  }
}

Gateway deny wire-codes (FS_DENY_CODES)

The canonical deny code values. Grouped by concern; the HTTP status the gateway returns alongside each is in the table.

codeHTTPMeaning
limit_exceeded429A usage/quota limit on a metered dimension is reached.
rate_limited429A per-window rate limit is reached. Retryable.
credit_exhausted402Prepaid credit balance is below what this request would spend.
enforcement_denied403A consume-phase batch enforcement check denied the request.
limit_allocator_unavailable503The limit allocator was transiently unavailable. Retryable.
feature_not_enabled403The plan/entitlement doesn't grant the requested feature.
invalid_entitlement_shape503The resolved entitlement failed schema validation. Retryable.
unsupported_constraint_schema503A constraint used an unsupported schema. Retryable.
enforcement_error500Enforcement hit an unexpected error.
enforcement_dependency_unavailable503An enforcement dependency was transiently unavailable. Retryable.
concurrency_limit_exceeded429The plan's concurrent-request cap is reached. Retryable.
concurrency_context_unavailable503Concurrency context couldn't be read. Retryable.
concurrency_coordinator_unavailable503The concurrency coordinator was unavailable. Retryable.
key_expired401The API key has expired.
geo_context_unavailable503Geo context couldn't be resolved. Retryable.
geo_blocked403The request origin is in a blocked region.
geo_not_allowed403The request origin isn't in the allow-list.
resource_count_limit_exceeded429A counted-resource cap (e.g. cron_jobs) is reached.
resolver_rate_limited429An internal resolver was rate-limited. Retryable.
resolver_unavailable503An internal resolver was unavailable. Retryable.
credential_resolver_miss_rate_limited429Credential-resolver miss path was rate-limited. Retryable.

Retryable codes

This subset is safe to auto-retry with backoff — the 429 throttles plus the transient 503 dependency faults. The public guards take the caught error, not a raw code: isRetryable(err) is true for any of these denies (it also folds in transient transport statuses), and isThrottled(err) narrows to the 429 back-off cases.

import { isRetryable, isThrottled } from "@farthershore/farthershore-js";

try {
  await fs.feature("cron-jobs").json("/v1/cron-jobs");
} catch (err) {
  if (isThrottled(err)) return backOffAndRetry(); // 429 rate/quota
  if (isRetryable(err)) return retry(); // + transient 503 deps
  throw err;
}

limit_allocator_unavailable, rate_limited, invalid_entitlement_shape, unsupported_constraint_schema, enforcement_dependency_unavailable, concurrency_limit_exceeded, concurrency_context_unavailable, concurrency_coordinator_unavailable, geo_context_unavailable, resolver_rate_limited, resolver_unavailable, credential_resolver_miss_rate_limited.

limit_exceeded and credit_exhausted are not retryable — the limit won't clear by retrying. Surface an upgrade or top-up affordance instead (see the limitCode field below).

Upgrade affordance — limitCode

A limit deny also carries a separate limitCode field (an upgrade-affordance value, not a wire code) telling the UI what kind of limit was hit. The fixed values:

limitCodeHit
quotaAn included-usage / hard-cap quota.
rate_limitA per-window rate limit.
creditThe prepaid credit balance.
resource:<name>A counted-resource cap, e.g. resource:cron_jobs (open family — match by the resource: prefix).

Runtime verification statuses

These come from the upstream's @farthershore/backend, not the gateway deny path — when fs.verifyRequest() / fs.middleware() rejects a request the gateway forwarded. Verification is fail-closed: every failure maps to one status.

HTTPWhen
401Any verification failure — missing / malformed / bad-signature / stale / clock-skew / wrong-route / body-hash-mismatch / replayed-nonce / unknown-kid / jwks-unavailable.
413The request body exceeds MAX_BODY_BYTES.

FartherShoreError.code carries the precise runtime reason (e.g. invalid_token, jwks_unavailable); statusForCode(code) maps it to the HTTP status above. There is no fail-open branch.

HTTP status taxonomy

How the statuses map to categories across both surfaces:

HTTPCategoryTypical codes
401Authenticationkey_expired, runtime verification failures
402Creditscredit_exhausted
403Authorizationfeature_not_enabled, enforcement_denied, geo_blocked, geo_not_allowed
413Payloadruntime oversized body
429Limits / throttlelimit_exceeded, rate_limited, concurrency_limit_exceeded, resource_count_limit_exceeded
500Runtimeenforcement_error
503Transient runtime*_unavailable, *_rate_limited (retryable)

How to debug a denial

  1. Read the code (not the message).
  2. Is it in the retryable set? If so, back off and retry.
  3. If it's a limit (limit_exceeded / credit_exhausted / resource_count_limit_exceeded), read limitCode and surface upgrade / top-up.
  4. If it's a 401 from your own upstream, it's a verification failure — check FartherShoreError.code and that FS_RUNTIME_TOKEN is current.
  5. Confirm the subscriber's key, subscription, and plan limits.