> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Refusal Records

> The evidence a rejected transaction leaves behind: a signed, independently verifiable Refusal Record, and the two scope caveats that bound the claim.

<Info>
  **\[AVAILABLE]**, shipped commit `f779d68` (2026-08-02), RFC-0021. See `docs/CLAIMS.md` §3.11
  for the full evidence list this page draws from.
</Info>

## The gap this closes

An **approved** execution has always left cryptographic evidence behind: the Execution Trust
Record and its signature (see [Execution Trust Records](/concepts/execution-trust-records)). A
**refused** one used to leave only an HTTP response and whatever the caller's own logs happened
to capture, nothing durable, nothing independently checkable. A Refusal Record is the REJECT-path
counterpart to an Execution Trust Record: durable, signed, and third-party-verifiable the same
way, without requiring a caller credential or database access to check it.

Scope is deliberately narrow. A Refusal Record is produced only for a policy decision that
`RuntimeEngine.execute` actually reaches: an ordinary `PolicyEngine.evaluate` REJECT, or a
`SignalIntentBinder` binding-violation REJECT (see [Policies and the
decision](/concepts/policies-and-the-decision)). Caller-authentication failures and webhook
signature failures are a separate, unsigned audit-sink capability (RFC-0021 Non-Goals) — treat
any claim about Refusal Records as "a policy decided to reject," not "every rejection of any
kind."

## What's in the record

```typescript theme={null}
// packages/shared/src/domain/refusal-record.ts
export interface RefusalRecord {
  readonly refusalRecordId: string;
  readonly businessTransactionId: string;
  readonly decision: Decision;                 // the exact rejected Decision, not summarized
  readonly evaluatedIntent: RefusalIntentSnapshot;
  readonly bindingViolations?: readonly RefusalBindingViolation[]; // present only for a SignalIntentBinder REJECT
  readonly submittedBy?: string;                // absent when caller-auth is disabled
  readonly refusalRecordHash: string;
  readonly signature: Signature;                // same signing stack as ExecutionTrustRecord
  readonly createdAt: Date;
}
```

`RefusalRecordBuilder` (`packages/runtime/src/RefusalRecordBuilder.ts`) builds the record from
the rejected transaction, its `Decision`, and any binding violations. `RefusalCrypto`
(`packages/crypto/src/RefusalCrypto.ts`) hashes and signs it with the same
`FileKeyProvider`/`DEFAULT_KEY_ID` stack every other signed artifact in this system uses — one
root of trust, not a separate one for refusals. At most one `RefusalRecord` exists per
`businessTransactionId`: unlike an Execution Trust Record, a refusal is a single terminal event,
not an append-only aggregate.

## Two routes, two different trust models

* **`POST /refusal/verify`** — verifies a submitted Refusal Record's signature, returns
  `{ valid }`. Deliberately mounted **ahead of** caller-auth middleware
  (`packages/api/src/app.ts`): no API key, no lookup by ID, nothing but the artifact itself and
  Parmana's public key. This is what makes a refusal independently checkable by whoever actually
  received the rejection, not only by Parmana.
* **`GET /refusal/:businessTransactionId`** — looks up a Refusal Record from Parmana's own
  storage by transaction ID. Unlike the route above, this one stays behind caller-auth and
  ownership scoping (`isOwnedByCaller`), identically to `/verify`, `/verification`, and
  `/trust-records` — the underlying transaction content (signals, intent parameters) may be
  sensitive, even though the signature-verification capability above is intentionally open.

## Two scope caveats, both load-bearing

<Warning>
  1. **Refusal Record writing fails open, deliberately — not fail-closed like caller-auth audit
     writes.** `RuntimeEngine.writeRefusalRecord` runs after the `Decision` is built but is
     explicitly barred from delaying or affecting the rejection response that follows it. A write
     failure is logged (`refusal_record_write_failed`) and swallowed, never thrown. **The rejection
     itself is unaffected either way** — a request that should be denied is still denied, correctly,
     whether or not its evidentiary record lands. What can be silently missing is the durable proof
     of *why*, not the correctness of the refusal. This was considered for change (making the write
     atomic/fail-closed with the response) and explicitly rejected on 2026-08-19: it would turn a
     correct policy rejection into an opaque `500` on a storage hiccup, a real availability
     regression, for no corresponding security gain — the request was already unconditionally
     denied the instant `Decision` was built.
  2. **Only production (Supabase) audit sinks sign.** `SupabaseCallerAuditSink` and
     `SupabaseRazorpayWebhookAuditSink` sign every event; the in-memory test sinks
     (`NODE_ENV=test`) do not. Existing rows written before this capability shipped remain
     unsigned — `signature_json` is nullable and additive, reflecting that history rather than
     backfilling a signature that was never actually produced.
</Warning>

## Related

* **`POST /audit/verify`** closes the analogous gap for caller-authentication and Razorpay-webhook
  audit trails — one route, verifies either a `CallerAuditEvent` or a
  `RazorpayWebhookAuditEvent`, also mounted ahead of caller-auth. See [Error
  catalog](/api-reference/error-catalog) for both routes' error shapes.
* [Execution Trust Records](/concepts/execution-trust-records) — the APPROVE-path counterpart.
* [Policies and the decision](/concepts/policies-and-the-decision) — how a `Decision` reaches
  REJECT in the first place, including `SignalIntentBinder` binding violations.
* `docs/rfcs/RFC-0021-Refusal-Record.md` in the repository — the original design document.
