# Request policy

[RightNow AI](https://www.rightnowai.co/) / [RightNow System Zero](https://www.rightnowai.co/docs/system-zero) / [Request policy](https://www.rightnowai.co/docs/system-zero/request-policy)

Updated 2026-11-07

Add an optional `policy` object to a RightNow System Zero decide request. Answers below your confidence threshold come back without a decision, and a request whose maximum possible cost exceeds your cap is refused before any work.

**Manual HTTP (decide.sh)**

```shell
# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
# Generate once per logical decision; reuse this key when retrying.
idempotency_key="$(uuidgen 2>/dev/null \
  || cat /proc/sys/kernel/random/uuid 2>/dev/null \
  || openssl rand -hex 16 2>/dev/null \
  || python3 -c 'from uuid import uuid4; print(uuid4())' 2>/dev/null)"
cat > request.json <<'EOF'
{
  "model": "system-zero",
  "state": "The blender I ordered arrived with a cracked jar. I need it working before Saturday.",
  "questions": {
    "damaged_in_transit": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    },
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": {
        "replacements": "Ship a new item or part",
        "refunds": "Return money to the customer",
        "support": null
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is this ticket?",
      "criteria": [
        "Routine",
        "Priority",
        "Immediate"
      ]
    }
  },
  "policy": {
    "min_confidence": 0.8,
    "on_low_confidence": "abstain",
    "max_cost_usd": 0.29
  }
}
EOF
# Headerless error responses require Python 3 to read error.code.
# Manual example policy: 130 seconds per attempt, a 600-second resend window
# with no retry-count cap. If x-should-retry is present, resend only when
# x-should-retry is true. If the header is absent, resend on 429, or on 409 and
# 5xx only with a retryable error.code. No answer also retries. Wait Retry-After
# plus up to 10 %, otherwise a random wait doubling from 1 s to 30 s. Stop if
# Retry-After is over 60 s. Also stop when a 429 has no Retry-After, or a 5xx
# has x-should-retry but no Retry-After.
# SDK defaults: The default is 5 retries, also the maximum, with 150 seconds per
# attempt and a 60-second total retry budget. A single wait longer than 60
# seconds raises the last error.
# Use Retry-After-Ms, or Retry-After when milliseconds are absent. The wait is
# never shorter than a random backoff floor: a random fraction of 0.5 seconds
# doubled for each attempt, capped at 8 seconds.
random() { od -An -N4 -tu4 /dev/urandom | tr -d ' '; }
# Stop resending after 10 minutes.
start=$(date +%s) attempt=0
while :; do
  : "${SYSTEM_ZERO_API_KEY:?set it first}"
  : "${idempotency_key:?could not make one}"
  rm -f reply.json
  out=$(curl https://api.rightnowai.co/v1/system-zero/decide \
    --max-time 130 -sS -o reply.json -D - -w '%{http_code}' \
    -H "Authorization: Bearer $SYSTEM_ZERO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $idempotency_key" \
    -d @request.json)
  code=$(printf '%s' "$out" | tail -c 3)
  retry_header=$(printf '%s\n' "$out" \
    | grep -i '^x-should-retry:' | tail -n 1)
  retry=$(printf '%s\n' "$retry_header" \
    | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  after=$(printf '%s\n' "$out" | grep -i '^retry-after:' \
    | tail -n 1 | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  case $after in *[!0-9]*|'') after= ;; esac
  if [ -z "$retry_header" ]; then
    case $code in
      000|429) ;;
      409|5??)
        error_code=$(python3 -c 'import json, sys
try:
    print(json.load(sys.stdin).get("error", {}).get("code", ""))
except (ValueError, AttributeError):
    pass' < reply.json 2>/dev/null) || break
        case $code:$error_code in
          409:idempotency_conflict) ;;
          5??:service_unavailable) ;;
          5??:system_zero_unavailable) ;;
          5??:billing_settlement_error) ;;
          5??:idempotency_unavailable) ;;
          5??:limiter_unavailable) ;;
          5??:internal_error) ;;
          5??:maintenance) ;;
          *) break ;;
        esac ;;
      *) break ;;
    esac
  else
    [ "$retry" = true ] || break
    case $code in 4??|5??) ;; *) break ;; esac
  fi
  if [ -n "$after" ]; then
    after=$(printf '%s' "$after" | sed 's/^0*//')
    after=${after:-0}
    [ "$after" -le 60 ] || break
    ms=$(( after * 1000 + $(random) % (after * 100 + 1) ))
  else
    [ "$code" != 429 ] || break
    if [ -n "$retry_header" ]; then
      case $code in 5??) break ;; esac
    fi
    cap=$(( attempt < 5 ? 1000 << attempt : 30000 ))
    ms=$(( $(random) % (cap + 1) ))
  fi
  [ $(( $(date +%s) - start + ms / 1000 )) -lt 600 ] || break
  sleep "$(( ms / 1000 )).$(printf '%03d' $(( ms % 1000 )))"
  [ $(( $(date +%s) - start )) -lt 600 ] || break
  attempt=$(( attempt + 1 ))
done
echo "HTTP $code"
cat reply.json 2>/dev/null
```

## Policy fields

- **`min_confidence`**: Optional. A finite number greater than 0 and at most 1. An answer whose `confidence` is below it abstains; equal confidence passes.
- **`on_low_confidence`**: Optional. Only `abstain` is accepted. An omitted value also abstains.
- **`max_cost_usd`**: Optional. A cap in US dollars, greater than 0 and at most 1,000, with at most eight decimal places.

Every member is optional, and an empty object is valid. Send `policy` in a decide request or in a stream's start request. The gateway applies it; the model never receives it.

`on_low_confidence: "error"` and `require_calibrated` are not available. A request that sets either, including `require_calibrated: false`, returns `400 invalid_request`.

The published SDKs have no policy support: their request types omit `policy`, and `decisionsFor` and `decisions_for` refuse an abstained answer. Send `policy` over HTTP and read `answers` yourself, as this guide shows. Playground runs send no policy.

## Validation

An invalid policy returns `400 invalid_request` with type `invalid_request_error` and `error.retryable: false`. A `null` policy, numeric strings, values that are not finite, wrong types and unknown members are invalid.

`error.param` is `policy` for a value that is not an object, `policy.<member>` for a known member and `policy.*` for an unknown one. A decision path that takes no policy refuses one with `error.param` `policy`.

The cap allows at most eight decimal places after exponent expansion, and trailing zeros do not count: `0.29`, `0.07` and `3e-8` are valid, and `1e-9` is not. The gateway reads the original JSON number, so digits that a JSON parser would round away still count.

## Abstained answers

After the model replies, the gateway compares each answer's `confidence` with `min_confidence`. An answer below the threshold keeps its other fields as evidence, loses `decision` and gains `abstained: true` and `abstention_reason: "low_confidence"`.

Passing answers and question refusals keep their usual shape. A successful reply echoes the `policy` you sent, with no defaults added. In this illustrative reply, `damaged_in_transit` passes and the two other answers abstain.

**answers and policy**

```json
{
  "answers": {
    "damaged_in_transit": {
      "type": "boolean",
      "probability": 0.93,
      "confidence": 0.93,
      "calibration_version": "unpublished",
      "decision": true,
      "alternatives": [
        {
          "decision": false,
          "confidence": 0.07
        }
      ],
      "calibrated": false
    },
    "team": {
      "type": "choice",
      "choice": "replacements",
      "probabilities": {
        "replacements": 0.71,
        "refunds": 0.21,
        "support": 0.08
      },
      "confidence": 0.71,
      "calibration_version": "unpublished",
      "alternatives": [
        {
          "decision": "refunds",
          "confidence": 0.21
        },
        {
          "decision": "support",
          "confidence": 0.08
        }
      ],
      "calibrated": false,
      "abstained": true,
      "abstention_reason": "low_confidence"
    },
    "urgency": {
      "type": "score",
      "score": 1.48,
      "legend": {
        "0": "Routine",
        "1": "Priority",
        "2": "Immediate"
      },
      "probabilities": {
        "0": 0.07,
        "1": 0.38,
        "2": 0.55
      },
      "confidence": 0.55,
      "calibration_version": "unpublished",
      "alternatives": [
        {
          "decision": "1",
          "confidence": 0.38
        },
        {
          "decision": "0",
          "confidence": 0.07
        }
      ],
      "calibrated": false,
      "abstained": true,
      "abstention_reason": "low_confidence"
    }
  },
  "policy": {
    "min_confidence": 0.8,
    "on_low_confidence": "abstain",
    "max_cost_usd": 0.29
  }
}
```

Never act on an abstained answer. Its `choice`, `score`, `probability`, `probabilities` and `alternatives` are model evidence, not a decision. Check `abstained` before you read `decision`.

## Cost cap

Before admission, the gateway compares the request's maximum possible cost with `max_cost_usd`. It converts the cap exactly, at 100,000,000 microcents per US dollar, and the bound includes media minimums. Equal cost passes.

A request over the cap returns `400 policy_max_cost_exceeded` before any model work. It is not counted toward monthly usage and is not charged. The bound can exceed the final cost, so a cap near the expected cost can refuse a request that would have cost less.

**error.json**

```json
{
  "error": {
    "message": "The request's maximum possible cost exceeds max_cost_usd. The request was not dispatched, counted toward monthly usage or charged.",
    "type": "invalid_request_error",
    "param": "policy.max_cost_usd",
    "code": "policy_max_cost_exceeded",
    "retryable": false,
    "details": {
      "policy": {
        "max_cost_usd": 0.0001
      },
      "counted": false,
      "estimated_cost_microcents": 120000,
      "max_cost_microcents": 10000
    }
  },
  "request_id": "request-id-from-response"
}
```

- **`error.details.counted`**: `false`: nothing was counted or charged.
- **`error.details.policy`**: The policy you sent.
- **`error.details.estimated_cost_microcents`**: The maximum possible cost of the request, rounded up to whole microcents.
- **`error.details.max_cost_microcents`**: Your cap in microcents.

This refusal has `error.retryable: false`. Raise `max_cost_usd` or send a smaller request. The amounts in this example are illustrative.

## Usage and billing

An abstained answer is charged exactly like any answered question. It counts toward the monthly cap and pays the normal per-answer question fee, alongside the request's other charges. See [Pricing](https://www.rightnowai.co/docs/system-zero/pricing).

Question refusals stay unanswered and are not charged. A request refused by the cost cap is neither counted nor charged.

## Streams

In a stream, send `policy` inside `start.request`. It stays fixed across state changes, and a mutation cannot alter it. A policy error arrives as an `error` event with `fatal: false` and does not close the stream. See [Streaming](https://www.rightnowai.co/docs/system-zero/streaming).

## Replay and stored data

Policy is part of the `Idempotency-Key` fingerprint. Changing the policy under the same key returns `400 idempotency_key_reuse`.

A keyed replay returns the original result, abstentions included, without new model work, counting or charges. See [Idempotency](https://www.rightnowai.co/docs/system-zero/idempotency).

Stored replies keep their policy and abstentions under the same 24-hour answer window and eviction limits as other replies. Usage records keep only a count of abstained questions, with no policy, threshold, question name, reason or answer.

## Next

[Streaming](https://www.rightnowai.co/docs/system-zero/streaming)
