---
title: "Resource filters"
description: "How to restrict which resources an API key can touch."
icon: "filter"
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

To control what an API key can do, you give it **scopes**. Each scope has two parts:

- **action**: `read`, `write`, `admin`, or `*` (any action).
- **resourceFilter**: a path that says *which resources* the action applies to.

The resource filter is the part you tune to limit a key. A scope of `write` on
`PLACE/Site/<siteId>/THING/#/#` lets the key write to any Thing in that one site, and
nothing else.

## Filter syntax

A filter is a `/`-separated path of resource segments, from the outside in:

```text
TYPE/qualifier/id/TYPE/qualifier/id/…
```

- Each resource is a **type token** (uppercase) followed by its qualifier and id segments.
- **`#` is the wildcard.** It matches any value in that segment.
- The path is always scoped to your **organization**. A key can't reach another org's data,
  so you don't write the org into the filter.

For example, `PLACE/Site/<siteId>/THING/Battery/#` reads as: any Battery Thing, in the site
`<siteId>`.

## Resource types

| Type | Segments | Example |
|------|----------|---------|
| `PLACE` | `PLACE/{placeType}/{placeId}` | `PLACE/Site/<siteId>` |
| `THING` | `THING/{thingType}/{thingId}` | `THING/Battery/<thingId>` |
| `TRANSACTION` | `TRANSACTION/{transactionType}/{transactionId}` | `TRANSACTION/CommerceInvoice/#` |
| `MONITOR` | `MONITOR/{monitorId}` | `MONITOR/#` |
| `INTEGRATION` | `INTEGRATION/{direction}/{type}/{integrationId}` | `INTEGRATION/INGRESS/#/#` |
| `SIMULATION` | `SIMULATION/{simulationId}` | `SIMULATION/#` |
| `DEFINITION` | `DEFINITION/{definitionType}/{definitionId}` | `DEFINITION/Metric/#` |
| `COMMERCE` | `COMMERCE` | `COMMERCE` |
| `ORGANIZATION` | `ORGANIZATION/{orgId}` | `ORGANIZATION/#` |
| `TENANT` | `TENANT` | `TENANT` |

Some segments only accept a fixed set of values, checked when you create the key:

- `placeType`: `Site` or `Fleet`.
- `definitionType`: `Metric`, `ThingType`, or `ThingTypeVersion`.
- `direction`: `INGRESS` or `EGRESS`.
- `transactionType`: a known transaction type (e.g. `CommerceInvoice`, `BatterySwapReservation`).

An unknown value is rejected. Use `#` when you don't want to pin a value.

## Wildcards

`#` in a scope matches any value in the request. A specific value must match exactly
(case-insensitive). Use `#` to widen a scope and specific ids to narrow it.

| Filter | Covers |
|--------|--------|
| `PLACE/#/#` | Every place (any type, any id) |
| `PLACE/Site/#` | Every site |
| `PLACE/Site/<siteId>` | One site |
| `THING/#/#` | Every Thing |
| `THING/Battery/#` | Every Battery |
| `THING/#/<thingId>` | One Thing, whatever its type |

## Nesting

Filters can nest a child resource under its parent. Only these parent → child pairs are valid:

| Parent | Child |
|--------|-------|
| `PLACE` | `THING` |
| `PLACE` or `THING` | `MONITOR` |
| any type (except `TRANSACTION`) | `TRANSACTION` |
| `ORGANIZATION` | `ORGANIZATION` |

So these are valid composite filters:

- `PLACE/Site/<siteId>/THING/#/#`: all Things in one site.
- `PLACE/Site/#/THING/Battery/#/MONITOR/#`: monitors on any Battery in any site.
- `PLACE/Fleet/<fleetId>/THING/#/#/TRANSACTION/#/#`: all transactions on Things in a fleet.

A Thing can also stand on its own (`THING/Battery/<thingId>`) when you don't need to pin the place.

## How matching works

Each protected endpoint declares the **action** it needs and the **resource** it touches.
At request time, the platform fills the resource ids in from the request, then checks your
key:

1. The scope's action must match the endpoint's action, or be `*`.
2. The scope's resource filter must **cover** the endpoint's resource: every segment is
   either `#` or an exact match, and the hierarchy lines up.

A key is allowed if **any one** of its scopes covers the request. Otherwise the request gets
`403 Forbidden`. Scopes are additive: list several to grant several areas.

## Configuring filters on a key

You set filters when you mint a key, in the `scopes` list:

```json
{
  "keyType": "External",
  "name": "depot-ingest-bot",
  "scopes": [
    { "action": "write", "resourceFilter": "PLACE/Site/<siteId>/THING/#/#" },
    { "action": "read",  "resourceFilter": "PLACE/Site/<siteId>/THING/#/#" }
  ],
  "allowedIpCidrs": [],
  "expiresAt": null
}
```

This key can read and write any Thing in `<siteId>`, and nothing else. Each filter is
validated for shape and type tokens when the key is created or updated; an invalid filter is
rejected. See [API fundamentals](/platform/api-fundamentals) for the rest of the API key
fields (key type, IP allow-list, expiry).

## Limitations

- The wildcard is `#`. The `*` wildcard is for the **action**, not the path.
- A filter must name a resource type. There is no bare global wildcard.
- `DEFINITION/#/#` is rejected: name a `definitionType` (e.g. `DEFINITION/Metric/#`).
- Type tokens (`placeType`, `definitionType`, `direction`, `transactionType`) are validated
  against known values. Ids are not: a filter can name an id that doesn't exist.
- Only the parent → child pairs above can nest.
- Every filter is bound to your organization. A key can't grant access to another org.
