Gates
Management API
Note
Gates is available on all paid plans.
The Management API works with the same gates the dashboard edits. Use it to create 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.
Authentication
All endpoints live under https://api.usercheck.com/v1/gates and expect your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
The key determines both the account and the environment 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.
Every API key on a plan with Gates can call these endpoints, including keys you only use for email and domain lookups. Anyone who holds a key can change or delete the gates in its environment and read its decision log, which includes the emails and IP addresses you sent. Keep your keys secret, and revoke any key you have shared.
Management requests do not consume credits. Only the Decision Endpoint consumes credits.
Rate limits
Management requests are rate limited per second at the same per-plan rate as the rest of the API, shared by all your account's keys, but in their own bucket: a burst of management calls does not slow down your decisions or data API lookups, and the other way around. Exceeding the limit returns 429 rate_limit_exceeded with a Retry-After header.
Requests with a missing, mistyped, or revoked key are limited to 5 per hour per IP address. Past that, they return 429 rate_limit_exceeded instead of 401 unauthorized, with a Retry-After of up to an hour. If you get a 429 on your very first requests, check your key. Requests with a valid key are not affected.
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 |
All endpoints follow the same conventions:
- Send request bodies as JSON with
Content-Type: application/json. Any other body returns400 invalid_json. - A single gate, rule, or decision is returned as the object itself. Lists are wrapped in a top-level
dataarray, and the paginated decision log addslinksandmetanext to it. - Gate, rule, and decision IDs are lowercase ULIDs. Decision IDs are also accepted in uppercase, the form
/v0returns. - Write requests reject unknown body keys with
422 validation_failed, including extra keys inside a condition. - Changes apply to new decisions within a few seconds.
- A read sent right after a write can briefly return the previous state. The write's own response always reflects the change, so use it instead of reading the object back.
Gates
The gate object
{
"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. |
created_at, updated_at |
string | ISO 8601 timestamps. |
List gates
GET /v1/gates
Returns every gate in the key's environment, newest first. The list is not paginated.
curl "https://api.usercheck.com/v1/gates" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"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 | Omit it to use the key's environment. If you send it, it must be the key's environment (development, staging, or production); any other value fails with 422 validation_failed. |
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.
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. There is no restore endpoint; contact support if you need a deleted gate back.
Decision requests for the deleted gate return 404 gate_not_found. Its decision log is no longer available through the API, neither the list nor individual decisions, so export any logs you need before deleting the gate.
Rules
The rule object
{
"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. Rules run in ascending order; the numbers can have gaps and need not start at 1. Change it with the reorder endpoint, 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. |
created_at, updated_at |
string | No | ISO 8601 timestamps. |
How rules combine into a decision, and every field a condition can check, are covered in Rules & Conditions.
Condition operators
Each condition is checked when you write it. A field that is not a supported field, an op the field's type does not accept, a value of the wrong shape, or an invalid regex pattern fails with 422 validation_failed, and nothing is saved. The operators, the value each one takes, and the 255-character limits are listed in Rules & Conditions.
List rules
GET /v1/gates/{gate_id}/rules
Returns every rule of the gate in evaluation order, disabled rules included. The list is not paginated.
Add a rule
POST /v1/gates/{gate_id}/rules
The body takes the writable fields of the rule object: name, action, and conditions are required. If omitted, match defaults to all; an explicit null or invalid value is rejected. The remaining fields are optional.
The new rule is added at the bottom of the gate, after its last rule. It is enabled unless you send "enabled": false. To move it, use Reorder rules.
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. Returns the updated rule.
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. Returns the updated rule.
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 list every rule of the gate exactly once, disabled rules included, in the new order:
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 validation_failed, and no order is changed.
Syncing rules from code
Each write applies to live decisions on its own. There is no transaction across calls, so a sync that stops halfway leaves the gate partly updated. To change a live gate safely, apply the changes in this order:
- Add each new rule with
"enabled": false. It is stored but skipped by decisions. - Call Reorder rules with the full list in the order you want, including the rules you will delete in step 4. Take the new ids from the responses of your
POSTcalls rather than listing the rules again right away. - Update the rules that changed, and enable the new ones with
PATCH {"enabled": true}. - Delete the rules you removed, last.
To switch a whole rule set at once, build it on a new gate, test it, then change the gate_id your application calls. Delete the old gate once nothing uses it.
A sync sends one request per change, so it counts against the per-second rate limit. On a 429, wait the number of seconds in Retry-After and send the same request again.
Decision log
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 are plain query parameters, each 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 action, anonymity, country, and period parameters are case-insensitive: action=BLOCK is equivalent to action=block, and country=ru to country=RU.
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.
curl "https://api.usercheck.com/v1/gates/01k2wjgq8rnv6t0aefy3s7xdhb/decisions?period=7d&action=block&country=HK" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "01k2wjh8y4vd0bs2r8q1kthvgr",
"action": "block",
"rule": {
"id": "01k2wjgqv9y1b7t3e8fs64dnkm",
"name": "Block disposable emails"
},
"domain": "mailinator.com",
"email": "[email protected]",
"ip": "1.1.1.1",
"country": "HK",
"anonymity": [],
"created_at": "2026-08-18T06:24:09+00:00"
}
],
"links": {
"next": null
},
"meta": {
"period": "7d",
"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.
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 as a /v1 Decision Endpoint response: 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 the /v1 response, or meta.request_id on /v0.
{
"id": "01k2wjh8y4vd0bs2r8q1kthvgr",
"gate_id": "01k2wjgq8rnv6t0aefy3s7xdhb",
"action": "block",
"rule": {
"id": "01k2wjgqv9y1b7t3e8fs64dnkm",
"name": "Block disposable emails",
"message": "Please use a permanent email address."
},
"input": {
"email": "[email protected]",
"ip": "1.1.1.1"
},
"signals": {
"email": {
"address": "[email protected]",
"disposable": true
},
"domain": {
"name": "mailinator.com",
"disposable": true
},
"ip": {
"address": "1.1.1.1"
}
},
"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. The example shortens them; the Decision Endpoint shows them in full.
A decision reaches the log a few moments after the decision response returns. Fetching it straight away can return 404 decision_not_found; retry after a short wait.
Errors
Errors return the matching HTTP status code and a JSON body with a stable error code and a human-readable message:
{
"error": "gate_not_found",
"message": "Gate not found."
}
Codes specific to these endpoints:
404 gate_not_found: Unknowngate_id, or a gate outside the key's environment404 rule_not_found: Unknownrule_id, or a rule belonging to a different gate404 decision_not_found: Unknowndecision_id409 gate_limit_reached: The environment already holds 100 gates409 rule_limit_reached: The gate already holds 200 rules422 invalid_cursor: Thecursorparameter is not a cursor from a previous response
Invalid input fails with 422 validation_failed and an errors object naming the fields at fault. On the decision log, this includes an unknown or malformed query parameter.
{
"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 400 invalid_json, 401 unauthorized, 403 pro_only, and 429 rate_limit_exceeded. See Errors for the full list.