# Policy Sets & Groups

Write your tenant's signing policy as versions — ordered rules with a verdict each —, check them, try them on your own history, and publish one through a second administrator. On the institutional security profile, the active set decides every signature.

## Overview

- **A policy set** is one version of your tenant's signing policy: an ordered list of rules. Each rule names the operations it applies to — their type, who starts them, which wallet signs, where they go, the chain and the asset, the amount in EUR, the time — and a verdict: `ALLOW`, `SCREEN`, `REQUIRE_APPROVAL`, `BLOCK` or `FREEZE`. The first rule an operation matches decides.
- Your administrators write a set as a **draft**, **validate** it, **simulate** it on the tenant's past operations, **submit** it, and **publish** it as the tenant's active set — through another administrator when the tenant has more than one. Every version is kept; an older one can be published again.
- The routes are `/v1/policy-sets`, on the caller's own tenant. Reading, validating and simulating: the tenant roles `owner`, `tenantadmin`, `non_signing_admin`, `auditor`, `security_admin` and `security_auditor`. Writing, submitting, publishing, withdrawing and rolling back: `owner`, `tenantadmin`, `non_signing_admin` and `security_admin` ([Roles](https://docs.aureahub.com/docs/authentication.md)). Any other role answers `403` `Tenant admin access required. Your role does not have permission to perform this action.`

## Enforced or Logged

What the active set does depends on your tenant's security profile, agreed with Aurea [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md); the simulation's `tenantProfile` says which yours is.

| Profile | The active set |
| --- | --- |
| `institutional` | **Enforced.** At every signing step — before anything is signed, handed out to sign, or sent — the active set decides, and the call answers as below. No rule matched, or no active set: `BLOCK`. An operation of type `RAW`, `TYPED_MESSAGE` or `KEY_EXPORT` needs a rule that names its type. |
| `legacy` — every other tenant | **Evaluated and logged, never enforced.** A published set refuses, holds and changes nothing: every payment is answered exactly as it would be without it. Aurea evaluates each signature against it and logs the verdict on its side — for every tenant, today. |

Use [Simulate](https://docs.aureahub.com/docs/policy-set-simulate.md) to see what a set would decide on your own history, under your profile and under `institutional`, before you publish it.

## When the Policy Decides

For an institutional tenant, at every call that signs with a key the platform holds for your tenant, prepares a transaction or a permit for a client-side or MPC wallet to sign, starts an MPC signing ceremony, or broadcasts what was signed: sends, token approvals, swaps (gasless ones too), dApp signatures and broadcasts, [Earn](https://docs.aureahub.com/docs/guide-earn.md), and the key export.

- **The key export is decided twice**: when its token is requested ([Request Export Token](https://docs.aureahub.com/docs/wallets-export-token.md) — a refusal leaves the wallet as it was, with no token) and when the key is exported ([Export Encrypted Key](https://docs.aureahub.com/docs/wallets-export-key.md)). It is a `KEY_EXPORT` operation: without a rule naming that type, it is blocked.
- **A broadcast is decided again.** An operation prepared for a client-side or MPC wallet is decided when it is prepared and again when what was signed is broadcast, or when an MPC wallet's signing ceremony starts.
- **The money routes need an `Idempotency-Key`** for an institutional tenant: `400` `IDEMPOTENCY_KEY_REQUIRED` without one ([Idempotency](https://docs.aureahub.com/docs/idempotency.md)).
- **Rolling windows are measured.** A window rule's total is the EUR amount of the operations that signed under the policy in the last `windowSeconds`, for the same initiator, source wallet or destination; an operation is counted once, even when it signs twice. An operation that ended without moving anything — `FAILED`, `BLOCKED`, `REJECTED`, `CANCELLED` — stops counting, and one without an EUR price is not counted. Two operations that would both fill the same window are decided one after the other: only one fits.
- **A recorded operation shows the decision** ([Operations & Fees](https://docs.aureahub.com/docs/guide-operations.md)): `PENDING_POLICY` `evaluated`, then `PENDING_AUTHORIZATION` `not_required` when it may sign, `awaiting_approval` while it waits for approvers, `approved` once approved; a block or a freeze ends it `BLOCKED` (`failureReason` `POLICY_BLOCKED` or `POLICY_FROZEN`), any other refusal `FAILED` `not_sent`.

## Refusals at Signing

For an institutional tenant only, before anything is signed, handed out to sign, or sent:

| Verdict | Status | details.code | What happens |
| --- | --- | --- | --- |
| ALLOW | — | — | Signed. The operation's EUR amount counts in the rules' rolling windows. |
| REQUIRE_APPROVAL | 202 | — | On the money routes that start an operation: held until the rule's approvers approve it — [Approvals](https://docs.aureahub.com/docs/guide-approvals.md). |
| REQUIRE_APPROVAL | 403 | POLICY_APPROVAL_REQUIRED | Everywhere else, unless the operation was approved for that rule. `The tenant's policy requires an approval for this operation, which cannot be held for approval here: nothing was signed or sent.` |
| BLOCK, or no rule | 403 | POLICY_BLOCKED | `The tenant's policy blocks this operation: nothing was signed or sent.` |
| ALLOW or REQUIRE_APPROVAL, by a rule naming designated signers | 403 | POLICY_SIGNER_NOT_DESIGNATED | **Where designated signers are on** (on request; Designated Signers below): the operation's initiator is not one of the rule's designated signers. Refused before anything is held, signed or sent. `The tenant's policy names who may sign this operation, and you are not one of them: nothing was signed or sent.` |
| FREEZE | 423 | POLICY_FROZEN | The source wallet is frozen as well, with `details.freezeId`: it signs nothing until one of your administrators lifts the freeze ([Freezes & Quarantines](https://docs.aureahub.com/docs/guide-freezes.md)). `The tenant's policy froze this wallet: nothing was signed or sent, and it cannot sign until the freeze is lifted.` |
| SCREEN, screening passed | — | — | Where Aurea screens for your tenant ([Screening & AML Cases](https://docs.aureahub.com/docs/guide-screening.md)): signed as an `ALLOW` once the operation's screening is `PASSED` (no match) or `BYPASSED` (a match your AML case released), and counted in the rolling windows. |
| SCREEN, no screening that passed | 503 | POLICY_SCREENING_UNAVAILABLE | No screening for your tenant, one that could not complete (`PENDING`), or a match screening only records (`FLAGGED`, in `log`): never signed. `The tenant's policy asks for a screening of this operation, which is not available: nothing was signed or sent.` |
| — | 503 | POLICY_UNAVAILABLE | The policy could not be evaluated in time, or at all: nothing is signed. Try again. `The tenant's policy could not be evaluated: nothing was signed or sent. Try again.` |

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "The tenant's policy blocks this operation: nothing was signed or sent.",
  "details": {
    "code": "POLICY_BLOCKED",
    "verdict": "BLOCK",
    "ruleId": "e2b6d9f1-7a3c-4c58-8b0e-5f1a4d2c9b73",
    "ruleName": "No contract deployments",
    "reason": "rule_matched"
  }
}
```

- `details.reason`: `rule_matched`; `missing_data` — a restrictive rule decided because the operation lacked what it checks, named in `details.missing` (e.g. `["amount"]`); `no_matching_rule`; `no_policy_set`; `explicit_rule_required` — a `RAW`, `TYPED_MESSAGE` or `KEY_EXPORT` operation no rule names; `engine_error`. `ruleId` and `ruleName` are `null` when no rule decided; `POLICY_UNAVAILABLE` names no verdict, rule or reason.
- The details never repeat a rule's content: no amount, address or person it names.
- On a broadcast the prepared transaction or permit was used before the refusal: prepare it again. A client-side send refused when it was prepared keeps its `pending` transaction record, which fails like any preparation never broadcast.
- A frozen wallet, user or tenant answers its own `423` first (`WALLET_FROZEN`, `USER_FROZEN`, `TENANT_FROZEN`).

## The Life Cycle

```text
draft ──submit──► awaiting_quorum ──publish──► active ──(another version published)──► archived
  │                      │
  └──────archive─────────┴──► archived
```

- **draft**: written by its author, the administrator who created it. Only the author replaces its rules and submits it; another administrator copies it into a draft of their own (`copyFromSetId`).
- **awaiting_quorum**: submitted, once valid. From here on a version never changes.
- **active**: the tenant's policy — one at a time. Publishing a set archives the one it replaces.
- **archived**: replaced, or withdrawn while a draft or awaiting. The active set is never withdrawn on its own: publish its replacement.
- **Rollback**: a version that was once active is copied, rules and all, into a new version, authored by the caller and submitted — published at once where the tenant's quorum is off, else waiting for another administrator.
- Each set is the tenant's next `version`, from 1. Who published a set (`activatedBy`) and who archived it (`archivedBy`) stay on it.

## Who Publishes

The administrators counted are the active users whose role writes policy sets (`owner`, `tenantadmin`, `non_signing_admin`, `security_admin`); a suspended one doesn't count.

| Your tenant | Who publishes a submitted set |
| --- | --- |
| One administrator | That administrator, alone: there is no one else to ask. |
| Two administrators or more | An administrator other than the set's author — `403` `POLICY_QUORUM_AUTHOR` for the author. The publication is recorded as that administrator's approval (`approverUserIds`). |
| Security profile `institutional` | Always an administrator other than the author: with a single administrator, the set waits until a second one exists. |

The count is taken when the set is submitted — `requiredApprovals`: `1` when another administrator must publish it, else `0` — and again when it is published, so the rule applies from the day a second administrator exists. Your tenant's security profile is set by Aurea: `legacy` unless Aurea made it `institutional` (the simulation's `tenantProfile` says which).

## A Passkey Step-Up

Before a set goes live — [published](https://docs.aureahub.com/docs/policy-set-publish.md), or an archived version [rolled back to](https://docs.aureahub.com/docs/policy-set-rollback.md) — the administrator who does it confirms it with their passkey: `stepUpToken` in the body, a `policy_set.publish` or `policy_set.rollback` step-up on that set ([Passkeys & Step-Up](https://docs.aureahub.com/docs/guide-passkeys.md)).

- **Where your tenant's step-up mode is `enforce`** — on request [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md); a tenant on the institutional profile by its profile —: without it `403` `STEP_UP_REQUIRED`, with one that is not for this action on this set `403` `STEP_UP_INVALID`; nothing is published.
- **In `log`**, every other tenant's: the right token is used, and a missing or wrong one refuses nothing.
- It is checked after the publication's own checks, and its token is used with the publication, or not at all. A rollback left `awaiting_quorum` is confirmed again by the administrator who publishes it.

## Rules

A rule has a `name` (1–200 characters) and a `verdict`; every condition is optional. A condition left out, or `null`, matches anything; a list holds 1 to 500 values. A set holds at most 500 rules, in order.

```json
{
  "rules": [
    {
      "name": "Large transfers need two approvers",
      "verdict": "REQUIRE_APPROVAL",
      "operationTypes": ["TRANSFER"],
      "amountMinRef": "10000",
      "approvalThreshold": 2,
      "approverIds": ["0d2f6b8a-4c1e-4a93-b7d5-2e9f1c3a8b60", "6a1c9e3f-2b7d-4f50-8e4a-0c5d3b9f7e12"]
    },
    { "name": "No contract deployments", "verdict": "BLOCK", "operationTypes": ["DEPLOY"] },
    { "name": "Everything else", "verdict": "ALLOW" }
  ]
}
```

| Field | Matches |
| --- | --- |
| verdict | `ALLOW`, `SCREEN`, `REQUIRE_APPROVAL`, `BLOCK` or `FREEZE`, from the least to the most restrictive. |
| operationTypes | `TRANSFER`, `CONTRACT_CALL`, `APPROVE`, `TYPED_MESSAGE`, `RAW`, `DEPLOY`, `KEY_EXPORT`. |
| initiatorKinds, initiatorIds | Who starts it: `user`, `group`, `api_credential`, `agent`, `platform`; and their ids — a [group](https://docs.aureahub.com/docs/guide-policy-sets.md)'s id for `group`. |
| sourceKinds, sourceIds | Whose key signs: `custodial`, `client_side`, `mpc`, `platform`, `group`; and the wallets' ids — for `group`, a group's id: the wallets of its members. |
| destinationKinds, destinationAddresses | Where it goes: `internal` (a wallet of the tenant), `one_time`, `address_book`, `contract`, `dapp`; and the addresses. `address_book` matches a destination that is an active entry of your [address book](https://docs.aureahub.com/docs/guide-address-book.md), whatever its own kind — a listed contract is both `contract` and `address_book` —, where the book is read at the signing points (`log` or `enforce`: every tenant's today); where it is off, `address_book` matches no destination. So a set can say "transfers only to the address book". |
| chains, assets | Chain slugs (`ethereum`, `base`, `solana`…); assets as `<chain>:<token address>` or `<chain>:native`. |
| amountMinRef, amountMaxRef | The amount in EUR, inclusive, as decimal text (up to 20 digits and 18 decimals). |
| windowScope, windowSeconds, windowMinRef, windowMaxRef | A rolling total per `initiator`, `source` or `destination` over 60 seconds to 366 days; scope, seconds and a minimum or a maximum go together. Measured for an institutional tenant (When the Policy Decides); in a simulation, and for any other tenant, the total is not known, and such a rule is only ever a possible match. |
| methodSelectors | Four-byte function selectors, `0x` and 8 hex digits. |
| timeDays, timeFromMinute, timeToMinute | ISO weekdays (1 = Monday) and minutes of the day, in UTC; `[from, to)`, wrapping past midnight when `from > to`. Both minutes or neither. |
| screeningResults | `PASSED`, `FLAGGED`, `PENDING`, `BYPASSED`: the operation's screening against the sanctions lists, where Aurea screens for your tenant ([Screening & AML Cases](https://docs.aureahub.com/docs/guide-screening.md)). Without a screening — none for your tenant, a call with no counterparty, or a simulation — such a rule is only ever a possible match. |
| approvalThreshold, approverIds, approversExcludeInitiator | `REQUIRE_APPROVAL` only, and required there: how many approvals (1–20), from whom, and whether the initiator is kept from approving its own (default `true`). Name users whose role approves — `approver`, `owner`, `tenantadmin`, `non_signing_admin` ([Approvals](https://docs.aureahub.com/docs/guide-approvals.md)) — or a [group](https://docs.aureahub.com/docs/guide-policy-sets.md) of them: its members are asked, and the threshold counts people. |
| designatedSignerIds | The only users who may sign what the rule lets through — users, or groups whose members may (Designated Signers and Groups, below). Left out or `null`: anyone may. |

- **A missing value never allows.** When an operation can't be checked against a condition — its EUR amount is unknown because its token has no price, for example — a rule that would allow it does not match, and a restrictive one counts as a possible match.
- **No rule matched**: the profile's default — `ALLOW` for `legacy`, `BLOCK` for `institutional`. Under `institutional`, `RAW`, `TYPED_MESSAGE` and `KEY_EXPORT` also need a rule that names them; a rule for every type covers them only when it blocks or freezes.
- A rule written wrong is refused with its reasons, never repaired: `400` `POLICY_RULES_INVALID`, each reason in `details.errors` with the rule's position.

## Designated Signers

A rule's `designatedSignerIds` name the only users who may sign what it lets through — an operation it allows, or holds for approval.

- [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md) Until your tenant has designated signers, the list is stored, validated and shown, and refuses no one.
- **Once on, for a tenant on the institutional security profile**: when the rule that decides names designated signers and the operation's initiator is not one of them, the call answers `403` `POLICY_SIGNER_NOT_DESIGNATED` — before anything is held for approval, signed or sent — and a recorded operation ends `BLOCKED` with that code. The details name the rule, never the people. For any other tenant it is only recorded, as every evaluation is.
- **Who signs** is the operation's initiator: the user who calls, or, for an operation approved after a hold, the user who started it. An agent, an API credential or the platform is never a designated signer.
- It counts only for `ALLOW` and `REQUIRE_APPROVAL`: a rule that blocks, freezes or asks for a screening answers as it always does. A rule naming none restricts no one.
- **A group may be named** (Groups, below): its members are designated signers — the initiator's groups are read when the operation is decided.

## Validate and Simulate

- [Validate](https://docs.aureahub.com/docs/policy-set-validate.md) checks the set as a whole and the people and wallets its rules name. **Errors** stop a submission: `APPROVAL_THRESHOLD_UNREACHABLE` (fewer distinct approvers than the threshold), `APPROVER_UNKNOWN`, `DESIGNATED_SIGNER_UNKNOWN` (neither users nor groups of the tenant). **Warnings** don't: `EMPTY_SET`, `SHADOWED` and `SHADOWED_UNDER_LEGACY` (a rule that a rule before it with no condition hides), `WINDOW_NOT_MEASURED` (a window rule: its total is not known to a simulation, nor outside the institutional profile), `SCREENING_NOT_AVAILABLE`, `SOURCE_WALLET_UNKNOWN`, `INITIATOR_USER_UNKNOWN`, `ADDRESS_BOOK_NOT_OBSERVED` (a rule names the destination kind `address_book` while your address book is off: it matches no destination by that kind, and an "only the address book" set would block everything under the institutional profile), and `GROUP_UNKNOWN` (an id named as a group of initiators or sources that is none of your tenant's groups, users or wallets). A group's members count towards a threshold.
- [Simulate](https://docs.aureahub.com/docs/policy-set-simulate.md) runs a set — a draft or any version — on the tenant's operations of the last `days` (default 30, at most 90), the newest `limit` of them (default 2000, at most 5000), each at its own time: counts per verdict and per rule, under your tenant's profile and under `institutional`, with up to 10 operation ids per rule. A set naming `address_book` reads each destination in your address book as it is now, and says how many were listed (`addressBook`). Read only; amounts in EUR from the token prices Aurea keeps. At most 10 calls a minute.

## Groups

Name a set of your tenant's users once — "Treasury signers", "Finance approvers" — and let your policy rules name the group instead of each person.

- **A group** is a named set of your tenant's users: a `name` (1 to 100 characters, trimmed, unique in your tenant whatever its case), an optional `description` (up to 500 characters) and its members. It starts empty.
- **A group grants no permission.** What a member may do is still its role's ([Roles](https://docs.aureahub.com/docs/authentication.md)); a group only lets a policy rule name several people at once.
- A member is a user of your tenant whose account is not deleted. A user may be in several groups.
- The routes are under **Groups** in the API Reference, on the caller's own tenant. Deleting a group removes its memberships; a rule that named it then names no one by it.
- Each change is written to your tenant's [audit trail](https://docs.aureahub.com/docs/audit-events.md) [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md): `group.created`, `group.changed` (the fields changed), `group.deleted` (how many members it had), `group.member_added` and `group.member_removed` (the user's id), category `governance` — never a group's name or description.

### Who Manages Them

- **Reading** the groups and their members: `owner`, `tenantadmin`, `non_signing_admin`, `auditor`, `security_admin` and `security_auditor` — the roles that read the policy sets.
- **Creating, changing and deleting** a group, and adding and removing members: `owner`, `tenantadmin`, `non_signing_admin` and `security_admin` — the roles that write the policy sets.
- Any other role answers `403`.

### In Policy Rules

A rule names a group by its `id`, among its initiators, its sources or its approvers:

| Where | What it means |
| --- | --- |
| `initiatorKinds` with `group`, the group's id in `initiatorIds` | An operation started by a member of the group matches. |
| `sourceKinds` with `group`, the group's id in `sourceIds` | An operation signed by a wallet of a member of the group matches. |
| the group's id in `approverIds` | The approval asks the group's members; `approvalThreshold` counts people, not groups. A user named directly stays named. |

```json
{
  "name": "Large transfers need two of Finance",
  "verdict": "REQUIRE_APPROVAL",
  "operationTypes": ["TRANSFER"],
  "amountMinRef": "10000",
  "approvalThreshold": 2,
  "approverIds": ["3b8e1f6a-2c4d-4e9f-a1b7-6d5c4e3f2a10"]
}
```

- **Membership is read when an operation is decided**: a member added later counts from then on, one removed no longer does. A deleted user is no longer asked as an approver.
- **An empty group names no one** — never "any approver".
- **A membership that cannot be read never allows**: a rule naming the group does not allow the operation, and a restrictive one counts as a possible match, as for any missing value.
- [Validate](https://docs.aureahub.com/docs/policy-set-validate.md) counts a group's members towards a threshold, and warns with `GROUP_UNKNOWN` when a rule names, as a group of initiators or sources, an id that is none of your tenant's groups, users or wallets. [Simulate](https://docs.aureahub.com/docs/policy-set-simulate.md) reads the groups as a decision would.
- A rule's designated signers may be a group: its members may sign what the rule lets through (Designated Signers, above).
- Like every rule, it decides for a tenant on the institutional security profile, and is evaluated and logged for any other (Enforced or Logged, above).

### Refusals

The usual envelope, with the code in `details.code`:

| Status | details.code | message |
| --- | --- | --- |
| 400 | GROUP_REQUEST_INVALID | `A group's name is 1 to 100 characters`, or `Invalid request` with each problem in `details.errors` — a field that doesn't belong, a description over 500 characters, an id that is not a UUID, a change that names neither the name nor the description (`(body): Change the name or the description`) |
| 404 | GROUP_NOT_FOUND | `No such group in the tenant` — also another tenant's group |
| 404 | GROUP_USER_NOT_FOUND | `No such user in the tenant` |
| 404 | GROUP_NOT_MEMBER | `The user is not a member` |
| 409 | GROUP_NAME_TAKEN | `The tenant has a group by that name` |
| 409 | GROUP_ALREADY_MEMBER | `The user is a member already` |

## Refusals

| Status | details.code | message |
| --- | --- | --- |
| 400 | POLICY_RULES_INVALID | `Invalid policy rules`, with `details.errors` |
| 400 | POLICY_REQUEST_INVALID | `Invalid request` (with `details.errors`), or `Give rules or copyFromSetId, not both` |
| 403 | POLICY_SET_NOT_AUTHOR | `Only the author of a draft edits or submits it: copy it into a draft of your own` |
| 403 | POLICY_QUORUM_AUTHOR | `The tenant's quorum applies: an administrator other than the author publishes this policy set` |
| 403 | STEP_UP_REQUIRED | `Confirm this with your passkey: ask POST /v1/auth/step-up/options for this action, then send its stepUpToken`, with `details.action` and `details.tenantId` — publishing or rolling back where the step-up is enforced |
| 403 | STEP_UP_INVALID | `This step-up token is not valid for this action: it is unknown, expired, used, or for something else`, with `details.action` and `details.tenantId` |
| 404 | POLICY_SET_NOT_FOUND | `Policy set not found` — also another tenant's set. |
| 404 | POLICY_COPY_SOURCE_NOT_FOUND | `The set to copy is not found` |
| 409 | POLICY_SET_NOT_DRAFT | `A policy set that is active is not edited: copy it into a new draft` (its status named) |
| 409 | POLICY_SET_NOT_AWAITING | `A policy set that is draft is not published: submit a draft first` (its status named) |
| 409 | POLICY_SET_ACTIVE | `The active policy set is archived only when another version is published in its place` |
| 409 | POLICY_SET_ARCHIVED | `The policy set is already archived` |
| 409 | POLICY_SET_NOT_ARCHIVED | `Only an archived version is rolled back to` |
| 409 | POLICY_SET_NEVER_ACTIVE | `This version was never active: copy it into a draft instead` |
| 422 | POLICY_SET_INVALID | `The policy set is not valid`, with the validation's `details.errors` |
| 503 | POLICY_SIMULATION_TIMEOUT | `The tenant's history could not be read in time: simulate a shorter window or fewer operations` |

Every `409` carries the set's status in `details.status`.

---

Web version: https://docs.aureahub.com/#guide-policy-sets
