Entitlements

Rulings

What a ruling from the Server SDK contains, how to choose between adjudicate() and adjudicateAsync(), and how to turn a refusal into a response.

A ruling is the Server SDK's answer to one question: may this buyer organization use this capability, in this quantity, right now. Your backend asks before it does the work and acts on the answer. This page covers what to ask, what comes back and what to do with it, for the Server SDK 0.1.0-beta.1, installed with npm install @shiprev/server-sdk-node@beta.

Ask for a ruling

ts
const ruling = shiprev.adjudicate({
  organization_id: claims.organization_id, // from the verified session token only
  capability_key: 'report_exports',
  quantity: 1,
  operation: 'write',
});
OptionWhat to pass
organization_idThe buyer organization from the verified session token. Never take it from a header, a parameter or the body: the caller controls those
capability_keyThe key of the capability, as defined in your catalog
quantityHow much of the allowance the call would use; 1 if omitted. 0 asks only whether the capability is granted
operation'write' for anything that consumes quota, changes state or costs money, otherwise 'read'. Required: a ruling without it is refused with invalid_request

The operation decides what happens while your application's region is unreachable: reads keep working from the last snapshot, writes are refused. Snapshot, mirror and offline covers how to choose it route by route.

adjudicate() or adjudicateAsync()

Both rule from the mirror, the copy of your buyer organizations' entitlements that the Server SDK keeps in memory. They differ in what happens when the mirror cannot decide on its own.

MethodReturnsWhen the mirror cannot decide
adjudicate()A ruling, synchronously, with no I/ORefuses: capability_not_granted when the mirror does not know the organization or the capability, quota_exhausted beyond the allowance
adjudicateAsync()A promise of a rulingWhile online, asks your application's region in those two cases and returns its answer, with source authoritative; without an answer, the refusal stands

Use adjudicate() where a ruling must never wait on the network, for example inside a loop or a hot path. Use adjudicateAsync() where an organization that has just subscribed, or a call that could be paid for as overage, should get the region's answer. The NestJS capability guard uses adjudicateAsync(). Why the region decides beyond the allowance is explained in Overage policies.

Read the ruling

FieldHow to use it
allowedGo ahead or refuse. This is the only field you need to act on
reasonPresent on a refusal; tells you which response to give. See the next section
remainingWhat is left of the allowance in the current period, before this call. It does not subtract the quantity you asked for
limitThe allowance of the current period
sourceWhere the ruling came from: realtime from the mirror while online, authoritative from the region, degraded from the last snapshot while offline, client_default when there was no basis for a ruling
adjudicated_atWhen the ruling was made, in milliseconds since the epoch

remaining and limit are present when the ruling was made from a capability the mirror or the region holds, and absent on refusals that had no such basis. Every capability a plan grants has a numeric allowance, so when they are present they are numbers. Which refusals carry them is listed in Rulings in the reference.

Log source with your refusals. A refusal with source degraded or client_default says that the process was offline or had no basis for a ruling, not that the customer's plan said no.

Turn a refusal into a response

Each reason calls for a different answer to your caller. The status codes below are a suggestion; ShipRev does not prescribe them.

reasonWhat it means for your callerA fitting response
quota_exhaustedThe call goes beyond what is left of the allowance and no overage was allowed, or, for a write, usage of the period was left unpaid; see Overage policies403, with a way to a larger plan
capability_not_foundThe region confirms the organization's plan does not include the capability403, with a way to a plan that includes it
capability_not_grantedThe mirror does not know the organization or the capability; adjudicateAsync() would have asked the region403; if it persists, check the capability key and the customer's plan
disconnected_writeA write while the region is unreachable503, and try again later
cold_start_deniedA read before anything was loaded, with the region unreachable503, and try again later
invalid_requestoperation missing or unknown, or quantity negative or not finiteA bug in your code; fix the call
rule.ts
import type { Ruling } from '@shiprev/server-sdk-node';

/** Maps a refused ruling to the answer your API gives; null when the call may go ahead. */
export function refusal(ruling: Ruling): { status: number; reason: string } | null {
  if (ruling.allowed) return null;
  switch (ruling.reason) {
    case 'disconnected_write':
    case 'cold_start_denied':
      return { status: 503, reason: ruling.reason };
    case 'invalid_request':
      throw new Error('Invalid ruling request'); // a bug, not a customer problem
    default:
      return { status: 403, reason: ruling.reason ?? 'denied' };
  }
}

The NestJS capability guard does not make this distinction: every refusal, including disconnected_write, becomes a 403. Where you want a different status, rule in the handler yourself; see Protect routes.

A ruling holds nothing

A ruling reads the allowance; it does not take any of it. Two requests ruled at the same moment can both be allowed against the same remaining, and the usage you report afterwards is counted by the region whatever the rulings said. Usage beyond the allowance is then settled as Overage policies describes.

When an overrun must not happen, reserve the amount instead of ruling on it: reserve() takes the amount from the allowance at the region before the work starts, holding credits for any part beyond it that can be paid for as overage, and is refused when the amount cannot be covered. See Reserve, confirm and release.

Check a capability without using it

To show or hide a feature, ask with quantity: 0 and operation: 'read'. The ruling says whether the capability is granted, uses nothing, and keeps working from the last snapshot while the region is unreachable.

ts
const canExport = shiprev.adjudicate({
  organization_id: claims.organization_id,
  capability_key: 'report_exports',
  quantity: 0,
  operation: 'read',
}).allowed;

Where to go from here

© 2026 ShipRev