---
title: Management API
description: Create and manage gates, rules, and decision logs over the REST API.
---

> [!NOTE]
> Gates is available on all paid plans.

The Management API works with the same gates the dashboard edits. Use it to provision gates from code, keep rules in sync with configuration you store in your own repository, or pull decision logs into your own tooling.

These endpoints configure gates. To evaluate one, call the [Decision Endpoint](/docs/gates/decision-endpoint).

## Authentication

All endpoints live under `https://api.usercheck.com/v1/gates` and expect your API key in the `Authorization` header:

```bash
Authorization: Bearer YOUR_API_KEY
```

The key determines both the account and the [environment](/docs/get-started/environments) you are working in. A key only sees gates in its own environment: a gate that belongs to another environment (or another account) returns `404 gate_not_found`. To manage a different environment, use a key issued for that environment.

Management requests do not consume [credits](/docs/get-started/credits). Only the Decision Endpoint consumes credits.

## Rate limits

Management requests are rate limited per second at the same [per-plan rate](/docs/api/rate-limits) as the rest of the API, but in their own bucket: a burst of management calls does not slow down your live decision or data API traffic, and the other way around. Exceeding the limit returns `429 rate_limit_exceeded` with a `Retry-After` header.

## Endpoints

| Method and path | Purpose |
|---|---|
| `GET /v1/gates` | List gates |
| `POST /v1/gates` | Create a gate |
| `GET /v1/gates/{gate_id}` | Get a gate |
| `PATCH /v1/gates/{gate_id}` | Update a gate |
| `DELETE /v1/gates/{gate_id}` | Delete a gate |
| `GET /v1/gates/{gate_id}/rules` | List rules |
| `POST /v1/gates/{gate_id}/rules` | Add a rule |
| `GET /v1/gates/{gate_id}/rules/{rule_id}` | Get a rule |
| `PUT /v1/gates/{gate_id}/rules/{rule_id}` | Replace a rule |
| `PATCH /v1/gates/{gate_id}/rules/{rule_id}` | Update part of a rule |
| `DELETE /v1/gates/{gate_id}/rules/{rule_id}` | Delete a rule |
| `POST /v1/gates/{gate_id}/rules/reorder` | Reorder rules |
| `GET /v1/gates/{gate_id}/decisions` | List decisions |
| `GET /v1/gates/{gate_id}/decisions/{decision_id}` | Get a decision |

A single gate, rule, or decision is returned as the object itself. Lists are wrapped in a top-level `data` array, and the paginated decision log adds `links` and `meta` next to it. Changes take effect immediately: the next decision request uses the rules as they exist at that moment.

## The gate object

```json
{
  "id": "01k2wjgq8rnv6t0aefy3s7xdhb",
  "name": "Signup Gate",
  "environment": "production",
  "default_action": "allow",
  "rules_count": 2,
  "created_at": "2026-08-12T09:15:27+00:00",
  "updated_at": "2026-08-12T09:15:27+00:00"
}
```

| Field | Type | Description |
|---|---|---|
| `id` | string | Unique identifier of the gate. Used in every URL below and in the Decision Endpoint URL. |
| `name` | string | Display name. |
| `environment` | string | The environment the gate belongs to. Always the key's environment. |
| `default_action` | enum | `allow`, `block`, or `challenge`. Applied when no rule matches. |
| `rules_count` | integer | Number of rules in the gate. |

## List gates

```
GET /v1/gates
```

Returns every gate in the key's environment, newest first. The list is not paginated.

```bash
curl "https://api.usercheck.com/v1/gates" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": [
    {
      "id": "01k2wjgq8rnv6t0aefy3s7xdhb",
      "name": "Signup Gate",
      "environment": "production",
      "default_action": "allow",
      "rules_count": 2,
      "created_at": "2026-08-12T09:15:27+00:00",
      "updated_at": "2026-08-12T09:15:27+00:00"
    }
  ]
}
```

## Create a gate

```
POST /v1/gates
```

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name, up to 255 characters. |
| `default_action` | enum | Yes | `allow`, `block`, or `challenge`. Required so the fallback decision is explicit. |
| `environment` | string | No | Must match the key's environment. The gate is created in the key's environment either way, so you can omit it. |

```bash
curl -X POST "https://api.usercheck.com/v1/gates" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Signup Gate", "default_action": "allow"}'
```

Returns `201` with the new gate.

A gate created through the API starts with no rules. This differs from the dashboard, where a new gate comes with a set of starter rules. Until you add a rule, every decision falls through to `default_action`.

Creating a gate fails with `409 gate_limit_reached` once the environment holds 100 gates.

## Get a gate

```
GET /v1/gates/{gate_id}
```

Returns the gate object. The rules themselves are served by the [rules endpoints](#the-rule-object).

## Update a gate

```
PATCH /v1/gates/{gate_id}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | No | New display name. |
| `default_action` | enum | No | `allow`, `block`, or `challenge`. |

Fields you omit keep their value. At least one writable field is required: a request naming none of them is rejected rather than answered with an unchanged gate. A gate's `environment` cannot be changed. Returns the updated gate.

## Delete a gate

```
DELETE /v1/gates/{gate_id}
```

Deletes the gate and all of its rules. Returns `204` with no body. Decision requests for a deleted gate return `404 gate_not_found`.

## The rule object

```json
{
  "id": "01k2wjgqv9y1b7t3e8fs64dnkm",
  "order": 1,
  "name": "Block disposable emails",
  "description": null,
  "message": "Please use a permanent email address.",
  "match": "all",
  "action": "block",
  "stop": true,
  "enabled": true,
  "conditions": [
    { "field": "email.disposable", "op": "eq", "value": true }
  ],
  "created_at": "2026-08-12T09:16:03+00:00",
  "updated_at": "2026-08-12T09:16:03+00:00"
}
```

| Field | Type | Writable | Description |
|---|---|---|---|
| `id` | string | No | Unique identifier of the rule. |
| `order` | integer | No | Position in the evaluation order. Changed with the [reorder endpoint](#reorder-rules), not by editing the rule. |
| `name` | string | Yes | Short label, up to 255 characters. Shown in the dashboard and in decision logs. |
| `description` | string or null | Yes | Why the rule exists, up to 255 characters. |
| `message` | string or null | Yes | Up to 255 characters. Returned in the decision response when this rule sets the decision. |
| `match` | enum | Yes | `all` requires every condition to match (AND), `any` requires at least one (OR). |
| `action` | enum | Yes | `allow`, `block`, or `challenge`. |
| `stop` | boolean | Yes | When `true`, a match ends evaluation and this rule's action is final. Defaults to `false`. |
| `enabled` | boolean | Yes | When `false`, the rule is skipped. Defaults to `true`. |
| `conditions` | array | Yes | One or more conditions, each an object with `field`, `op`, and `value`. |

How rules combine into a decision, and the full list of condition fields, are covered in [Rules & Conditions](/docs/gates/rules-conditions).

### Condition operators

The operators a condition accepts depend on the field's type:

| Field type | Operators |
|---|---|
| boolean | `eq`, `ne` |
| number | `eq`, `ne`, `lt`, `lte`, `gt`, `gte` |
| string | `eq`, `ne`, `contains`, `starts_with`, `ends_with`, `regex`, `not_regex`, `in`, `not_in`, `is_null`, `not_null` |

The `value` follows the field and operator:

- Boolean fields take `true` or `false`; number fields take an integer.
- `in` and `not_in` take an array of strings, for example `{"field": "domain.tld", "op": "in", "value": ["xyz", "top"]}`.
- `regex` and `not_regex` take a bare PCRE pattern, such as `^admin$`. Do not wrap it in delimiters or append flags: `/^admin$/` and `/foo/u` are treated as pattern text, not as delimited expressions. Matching is case-insensitive by default (`i`); Unicode mode (`u`) is not enabled. PCRE inline options inside the pattern, such as `(?s)` or `(?-i)`, are supported.
  By default, `.` and its quantifiers count bytes, not Unicode code points: `^.{21,}$` matches a string of at least 21 bytes, so it already matches 11 Cyrillic letters encoded as 22 UTF-8 bytes.
- `is_null` and `not_null` need no `value`.
- String comparisons are case-insensitive by default.

## List rules

```
GET /v1/gates/{gate_id}/rules
```

Returns every rule of the gate in evaluation order. The list is not paginated.

## Add a rule

```
POST /v1/gates/{gate_id}/rules
```

The body takes the writable fields of the [rule object](#the-rule-object): `name`, `match`, `action`, and `conditions` are required, the rest are optional. The new rule is appended after the gate's last rule.

```bash
curl -X POST "https://api.usercheck.com/v1/gates/01k2wjgq8rnv6t0aefy3s7xdhb/rules" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Block disposable emails",
    "message": "Please use a permanent email address.",
    "match": "all",
    "action": "block",
    "stop": true,
    "conditions": [
      { "field": "email.disposable", "op": "eq", "value": true }
    ]
  }'
```

Returns `201` with the new rule, including its assigned `order`.

Adding a rule fails with `409 rule_limit_reached` once the gate holds 200 rules.

## Get a rule

```
GET /v1/gates/{gate_id}/rules/{rule_id}
```

Returns the rule object.

## Replace a rule

```
PUT /v1/gates/{gate_id}/rules/{rule_id}
```

Replaces the whole rule. `name`, `match`, `action`, and `conditions` are required. Fields you omit are reset to their defaults: `description` and `message` to `null`, `stop` to `false`, `enabled` to `true`. The rule's `order` is not affected.

## Update part of a rule

```
PATCH /v1/gates/{gate_id}/rules/{rule_id}
```

Changes only the fields you send; everything else keeps its value. At least one writable field is required. `conditions` is replaced as a whole when present: there is no way to edit a single condition in place.

```bash
curl -X PATCH "https://api.usercheck.com/v1/gates/01k2wjgq8rnv6t0aefy3s7xdhb/rules/01k2wjgqv9y1b7t3e8fs64dnkm" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
```

## Delete a rule

```
DELETE /v1/gates/{gate_id}/rules/{rule_id}
```

Deletes the rule. Returns `204` with no body. The remaining rules keep their relative order. Decisions the rule made stay in the decision log and keep reporting its id and name.

## Reorder rules

```
POST /v1/gates/{gate_id}/rules/reorder
```

Sets the evaluation order of the whole gate in one call. `rule_ids` must contain the id of every rule in the gate exactly once, in the desired order:

```bash
curl -X POST "https://api.usercheck.com/v1/gates/01k2wjgq8rnv6t0aefy3s7xdhb/rules/reorder" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rule_ids": [
      "01k2wjgqvatb5x0hry2c9pmesw",
      "01k2wjgqv9y1b7t3e8fs64dnkm"
    ]
  }'
```

Returns the gate's rules in their new order. A list that misses a rule, repeats one, or includes a rule from another gate fails with `422` and no order is changed.

## List decisions

```
GET /v1/gates/{gate_id}/decisions
```

Returns the gate's decision log, newest first, with cursor pagination.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `period` | enum | No | Lookback window: `24h`, `7d`, or `30d` (default). |
| `per_page` | integer | No | Results per page, 1 to 100. Defaults to 25. |
| `cursor` | string | No | Opaque cursor from a previous response. Omit for the first page. |

### Filters

Every filter is a plain query parameter matching one exact value. Filters on different fields combine with AND:

| Parameter | Value |
|---|---|
| `action` | `allow`, `block`, or `challenge` |
| `rule_id` | A rule id of the gate, or `default` for decisions that fell through to the gate's default action. Ids of deleted rules stay valid, since their decisions outlive them. |
| `domain` | A domain. Normalized before matching, so case and format variants find the same rows. |
| `email` | A complete email address. |
| `ip` | An IP address. |
| `anonymity` | `vpn`, `proxy`, `tor`, or `relay`: decisions whose IP had that anonymity type detected. |
| `country` | A two-letter country code. |

The parameters are validated strictly: a value that cannot match anything (a malformed email, an unknown rule id, a made-up anonymity type) fails with `422`, and so does a query parameter the endpoint does not know. A misspelled filter can therefore never answer `200` with an unfiltered list.

```bash
curl "https://api.usercheck.com/v1/gates/01k2wjgq8rnv6t0aefy3s7xdhb/decisions?period=7d&action=block&country=RU" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Response

```json
{
  "data": [
    {
      "id": "01K2WJH8Y4VD0BS2R8Q1KTHVGR",
      "action": "block",
      "rule": {
        "id": "01k2wjgqv9y1b7t3e8fs64dnkm",
        "name": "Block disposable emails"
      },
      "domain": "mailinator.com",
      "email": "user@mailinator.com",
      "ip": "1.1.1.1",
      "country": "HK",
      "anonymity": [],
      "created_at": "2026-08-18T06:24:09+00:00"
    }
  ],
  "links": {
    "next": null
  },
  "meta": {
    "period": "30d",
    "per_page": 25,
    "next_cursor": null
  }
}
```

List entries are summaries. `rule` is the rule that decided the entry, or `null` when the decision fell through to the gate's `default_action`. `country` is the two-letter country code of the evaluated IP, and `anonymity` lists what was detected on it, from `vpn`, `proxy`, `tor`, and `relay`. The full input and evaluated signals are returned by [Get a decision](#get-a-decision).

To fetch the next page, pass `meta.next_cursor` as the `cursor` parameter, or follow `links.next`. A `next_cursor` of `null` means there are no further pages. A malformed cursor fails with `422 invalid_cursor`.

## Get a decision

```
GET /v1/gates/{gate_id}/decisions/{decision_id}
```

Returns one decision in full, in the same shape the [Decision Endpoint](/docs/gates/decision-endpoint) returned when the decision was made: your application can parse one schema whether it is handling a live decision or reading one back. The decision id is the `id` field of that response.

```json
{
  "id": "01K2WJH8Y4VD0BS2R8Q1KTHVGR",
  "gate_id": "01k2wjgq8rnv6t0aefy3s7xdhb",
  "action": "block",
  "rule": {
    "id": "01k2wjgqv9y1b7t3e8fs64dnkm",
    "name": "Block disposable emails",
    "message": "Please use a permanent email address."
  },
  "input": {
    "email": "user@mailinator.com",
    "ip": "1.1.1.1"
  },
  "signals": {
    "email": { ... },
    "domain": { ... },
    "ip": { ... }
  },
  "duration_ms": 52.62,
  "created_at": "2026-08-18T06:24:09+00:00"
}
```

`rule.name` and `rule.message` are the wording captured when the decision was made; editing a rule later does not rewrite them. `input` is the request body the decision was made for, and `signals` holds the email, domain, and IP signal objects the rules ran against.

Decisions are written to the log asynchronously, moments after the decision response returns. Fetching a decision immediately after receiving it can return `404 decision_not_found` for that short window; retrying resolves it.

## Errors

Errors return the matching HTTP status code and a JSON body with a stable `error` code and a human-readable `message`:

```json
{
  "error": "gate_not_found",
  "message": "Gate not found."
}
```

Codes specific to these endpoints:

- `404 gate_not_found`: Unknown `gate_id`, or a gate outside the key's environment
- `404 rule_not_found`: Unknown `rule_id`, or a rule belonging to a different gate
- `404 decision_not_found`: Unknown `decision_id`
- `409 gate_limit_reached`: The environment already holds 100 gates
- `409 rule_limit_reached`: The gate already holds 200 rules
- `422 invalid_cursor`: The `cursor` parameter is not a cursor from a previous response

Invalid input, including an unknown or malformed query parameter, fails with `422 validation_failed` and an `errors` object naming the fields at fault:

```json
{
  "error": "validation_failed",
  "message": "Some input fields are invalid.",
  "errors": {
    "name": ["The name field is required."]
  }
}
```

On top of those, these endpoints return the codes shared by every Gates endpoint, including `401 unauthorized`, `403 pro_only`, and `429 rate_limit_exceeded`. See [Errors](/docs/gates/errors) for the full list.
