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

# Set Your Rules

> Set the policy your KYA SDK applies to each request, and manage the browser keys baselayer.js uses.

For engineers. Covers the settings behind acceptance: the policy your KYA SDK runs on each request, the origins baselayer.js can report from, and browser keys. Every route takes your API key in `X-API-Key` and acts on that key's application.

To set which audiences your credential check accepts, see [Audiences](https://docs.baselayer.com/docs/kya-sdk-quickstart.md#audiences).

## The Policy

The policy decides what your KYA SDK recommends for each request. It is a list of steps, such as `browse`, `cart` and `checkout`. Each step names the requests it covers and holds rules tried in order; the first rule that matches decides. Steps are also tried in order, so put the most specific first.

A rule matches on `transport`, `kind`, `trust` and `minimum_identity_level` (`L1` when you need to know which platform runs the agent, `L2` the same person each time, `L3` a verified person or business); a field left out matches anything. Its `outcome` is `allow`, `challenge`, `step_up` (step up: ask the agent for stronger proof, such as a KYA credential) or `block`, and `step_up` and `challenge` carry a `requirement`. A step's `enforcement_mode` is `report_only`, where decisions are recorded and never applied, or `enforce`. `detection_fallback` is what a step decides on when no detection result is available: `local_rules`. `conversions` say which requests count as a sale.

### What Step Up Means

Your KYA SDK returns a `step_up` decision with its `requirement`, and your server answers the agent. When the requirement is a `scope` and the agent carries a KYA credential, answer with a `401` and a `KYA-Disclosure-Scope` header naming the scope. The rule's `scope` is a scope from your KYA profile. See [Asking for Fields](https://docs.baselayer.com/docs/kya-asking-for-fields.md) and [Accepting Agents](https://docs.baselayer.com/docs/kya-accepting-agents.md). When the requirement is an `identity_level`, the agent needs a KYA credential that meets it or goes beyond it. For other agents, treat `step_up` as a block or let them through. Asking a person to verify on the spot is coming soon.

Handle `challenge` the same way: the decision is recorded, and your server decides what the agent gets.

### Steps and Matching

A step's `name` is one of these, and each name is used at most once:

| `name` |
| - |
| `browse` |
| `search` |
| `cart` |
| `checkout` |
| `payment` |
| `account` |
| `signup` |
| `write` |

A step's `match` says which requests it covers:

| Member | Matches |
| - | - |
| `paths` | Path patterns. `*` matches one segment and `**` any number. |
| `methods` | HTTP methods, applied to `paths` only. Empty means any. |
| `mcp_tools` | MCP tool names called by an agent. `*` matches any characters. |
| `ucp_operations` | Commerce operation names called by an agent, such as `checkout.complete`, `.`-separated, with the same wildcards as `paths`. |

An empty list matches nothing, so a plain web site leaves `mcp_tools` and `ucp_operations` as `[]`. A rule's `minimum_identity_level` needs policy schema version 2: a policy that uses it reports `minimum_policy_schema_version: 2`, and an older SDK runs it report-only.

A new application starts on policy version 1, Baselayer's default, where every step is `report_only` (watch-only). Your KYA SDK picks up a new version with no deploy; [How It Gets Your Rules](https://docs.baselayer.com/docs/kya-sdk-quickstart.md#how-it-gets-your-rules) says when.

### Read the Current Policy

Needs the `acceptance_policy:read` permission.

```bash theme={null}
curl -i "https://api.baselayer.com/acceptance/policy" \
  -H "X-API-Key: $BASELAYER_API_KEY"
```

The response carries an `ETag` header. Keep it: you send it back to replace the policy. To poll for a change, send it as `If-None-Match`: the answer is `304` with no body until the policy or your audiences change.

> [accordion] Response
>
> ```json theme={null}
> {
>   "policy_version_id": "87e11d3c-d5c3-4ab8-a2cd-a6238b2f47fe",
>   "version_number": 2,
>   "environment": "production",
>   "policy_hash": "sha256:d3c18d1cf04c69761b451e4f26a78612291c05b62a68ab282811aa1ee9899c5d",
>   "created_at": "2026-05-04T14:00:00Z",
>   "created_by": {
>     "kind": "API_KEY",
>     "label": "Policy automation"
>   },
>   "minimum_policy_schema_version": 1,
>   "policy": {
>     "steps": [
>       {
>         "name": "checkout",
>         "match": {
>           "paths": [
>             "/checkout/place"
>           ],
>           "methods": [
>             "POST"
>           ],
>           "mcp_tools": [
>             "checkout"
>           ],
>           "ucp_operations": []
>         },
>         "enforcement_mode": "report_only",
>         "rules": [
>           {
>             "name": "people",
>             "when": {
>               "transport": null,
>               "kind": [
>                 "human"
>               ],
>               "trust": null
>             },
>             "outcome": "allow",
>             "requirement": null
>           },
>           {
>             "name": "verified-person",
>             "when": {
>               "transport": null,
>               "kind": null,
>               "trust": [
>                 "credentialed"
>               ]
>             },
>             "outcome": "allow",
>             "requirement": null
>           },
>           {
>             "name": "agents-bring-a-person",
>             "when": {
>               "transport": null,
>               "kind": null,
>               "trust": null
>             },
>             "outcome": "step_up",
>             "requirement": {
>               "kind": "scope",
>               "scope": "shop-example#checkout",
>               "challenge": null
>             }
>           }
>         ],
>         "detection_fallback": "local_rules"
>       },
>       {
>         "name": "browse",
>         "match": {
>           "paths": [
>             "/**"
>           ],
>           "methods": [],
>           "mcp_tools": [
>             "*"
>           ],
>           "ucp_operations": []
>         },
>         "enforcement_mode": "enforce",
>         "rules": [
>           {
>             "name": "everyone",
>             "when": {
>               "transport": null,
>               "kind": null,
>               "trust": null
>             },
>             "outcome": "allow",
>             "requirement": null
>           }
>         ],
>         "detection_fallback": "local_rules"
>       }
>     ],
>     "conversions": [
>       {
>         "name": "order",
>         "activity_type": "purchase",
>         "event_kind": "purchase",
>         "match": {
>           "paths": [
>             "/checkout/place"
>           ],
>           "methods": [
>             "POST"
>           ],
>           "mcp_tools": [],
>           "ucp_operations": []
>         },
>         "response_statuses": [
>           200,
>           201
>         ]
>       }
>     ]
>   },
>   "warnings": [],
>   "audiences": [
>     "shop.example"
>   ]
> }
> ```

`policy_hash` is the hash of the policy body. Each decision your SDK reports names the version it ran, so you can always tell which policy made it.

### Replace the Policy

Needs the `acceptance_policy:update` permission. Send the whole policy, exactly as a read returns it under `policy`, with the `ETag` you read as `If-Match`:

```bash theme={null}
curl -X PUT "https://api.baselayer.com/acceptance/policy" \
  -H "X-API-Key: $BASELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "<etag from the read>"' \
  -d @policy.json
```

`policy.json`:

```json theme={null}
{
  "steps": [
    {
      "name": "checkout",
      "match": {
        "paths": [
          "/checkout/place"
        ],
        "methods": [
          "POST"
        ],
        "mcp_tools": [
          "checkout"
        ],
        "ucp_operations": []
      },
      "enforcement_mode": "report_only",
      "rules": [
        {
          "name": "people",
          "when": {
            "kind": [
              "human"
            ]
          },
          "outcome": "allow"
        },
        {
          "name": "verified-person",
          "when": {
            "trust": [
              "credentialed"
            ]
          },
          "outcome": "allow"
        },
        {
          "name": "agents-bring-a-person",
          "when": {},
          "outcome": "step_up",
          "requirement": {
            "kind": "scope",
            "scope": "shop-example#checkout"
          }
        }
      ],
      "detection_fallback": "local_rules"
    },
    {
      "name": "browse",
      "match": {
        "paths": [
          "/**"
        ],
        "methods": [],
        "mcp_tools": [
          "*"
        ],
        "ucp_operations": []
      },
      "enforcement_mode": "enforce",
      "rules": [
        {
          "name": "everyone",
          "when": {},
          "outcome": "allow"
        }
      ],
      "detection_fallback": "local_rules"
    }
  ],
  "conversions": [
    {
      "name": "order",
      "activity_type": "purchase",
      "event_kind": "purchase",
      "match": {
        "paths": [
          "/checkout/place"
        ],
        "methods": [
          "POST"
        ],
        "mcp_tools": [],
        "ucp_operations": []
      },
      "response_statuses": [
        200,
        201
      ]
    }
  ]
}
```

The response is the new current version. If someone else replaced the policy after your read, the replace is refused with `412` `policy_version_stale` and nothing changes: read it again and reapply your change. A request with no `If-Match` is refused with `428` `if_match_required`.

A policy that is valid but probably not what you meant, such as a rule no request can reach, is stored with `warnings` that say why.

### Earlier Versions

Versions are never changed or deleted. `GET /acceptance/policy/versions` lists them, newest first, and `GET /acceptance/policy/versions/{policy_version_id}` returns one exactly as it ran, which is the version a decision's `policy_version_id` names.

## Allowed Origins

Your application's allowed origins are the origins baselayer.js may send records from. Each is `https://` and a host, such as `https://shop.example`, or `http://` on a loopback host for local testing, such as `http://localhost:8080`, with no path and no default port. A record from any other origin is refused, and an application with no allowed origins accepts none. Your Baselayer team sets them: send them every origin your pages are served from.

Treat browser records as reports, not proof: anyone can copy your browser key and claim an allowed origin.

## Browser Keys

A browser key lets baselayer.js send records for your application. It is public: it ships in your page. An application has at most 2 live keys, so you can roll one over without a gap.

### Mint a Key

Needs the `organizations_apikeys:create` permission. `label` says what the key is for:

```bash theme={null}
curl -X POST "https://api.baselayer.com/acceptance/browser_keys" \
  -H "X-API-Key: $BASELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "shop.example"}'
```

```json theme={null}
{
  "browser_key_id": "5b0c7e2a-1f3d-4a6b-9c8e-2d4f6a8b0c1e",
  "label": "shop.example",
  "environment": "production",
  "created_at": "2026-05-04T14:05:00Z",
  "created_by": {
    "kind": "API_KEY",
    "label": "Setup key"
  },
  "revoked_at": null,
  "browser_key": "blpk_8kQ2xV7mN4pR9sT1uW3yZ5aB6cD8eF0gH2iJ4kL6mN8"
}
```

`browser_key` is in this response only. Baselayer cannot show it again, so store it now. Baselayer accepts a new key usually within about 20 seconds.

### List and Read Keys

Needs `organizations_apikeys:read`. `GET /acceptance/browser_keys` lists every key, live and revoked, newest first, and `GET /acceptance/browser_keys/{browser_key_id}` returns one. Neither ever shows the key itself, so tell keys apart by `label` and `browser_key_id`.

### Rotate a Key

`POST /acceptance/browser_keys/{browser_key_id}/rotate` mints a new key with the same label and needs `organizations_apikeys:create`. The old key stays live, so deploy the new key to your pages first, then revoke the old one. It answers `409` with `type` `browser_key_limit_reached` when there are already 2 live keys (revoke one first), and `browser_key_revoked` when the key you rotate is revoked (mint a new key instead).

### Revoke a Key

`POST /acceptance/browser_keys/{browser_key_id}/revoke` needs `organizations_apikeys:delete`. Baselayer stops accepting the key usually within about 20 seconds. Revoking a revoked key changes nothing.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.