# Validation

> Reference for the POST /validate endpoint used by every Centinel integration.

Source: https://docs.centinelanalytica.com/api/validation

## POST /validate

Validate a request

Returns a decision for one HTTP request: `allow`, `block`, or `redirect`. Call this endpoint from your backend for every protected request.

See the [integration guide](https://docs.centinelanalytica.com/install/validation.md) for backend handling patterns.

Servers:

- `https://validator.centinelanalytica.com` (Production)

Authentication, any one of:

- `apiKey`: apiKey in the `x-api-key` header. Your secret API key. Server-only — never call /validate from a browser.

### Request body

Content type `application/json`, required.

Schema:

```json
{
  "type": "object",
  "required": [
    "url",
    "ip"
  ],
  "properties": {
    "url": {
      "type": "string",
      "format": "uri",
      "description": "Full URL the user requested.",
      "example": "https://example.com/article"
    },
    "method": {
      "type": "string",
      "description": "HTTP method (GET, POST, etc). Optional, but strongly recommended for detection accuracy.",
      "example": "GET"
    },
    "ip": {
      "type": "string",
      "description": "Real client IP. If you are behind a CDN, use X-Forwarded-For (not the proxy IP).",
      "example": "1.2.3.4"
    },
    "headers": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "description": "All request headers. More headers improve detection accuracy. Optional, but detection accuracy drops sharply without them."
    },
    "referrer": {
      "type": "string",
      "description": "Value of the `Referer` request header.",
      "example": "https://example.com"
    },
    "cookie": {
      "type": "string",
      "description": "The `_centinel` cookie value, as a bare UUID rather than the whole `Cookie` header. A malformed value is silently ignored. If omitted, the validator falls back to extracting `_centinel` from the `Cookie` request header."
    }
  }
}
```

Example:

```json
{
  "url": "https://example.com/article",
  "method": "GET",
  "ip": "1.2.3.4",
  "referrer": "https://example.com",
  "cookie": "<_centinel-cookie-value>",
  "headers": {
    "Host": "example.com",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none"
  }
}
```

### Responses

#### 200

Valid request. `decision` holds the decision. `status_code` holds the HTTP status to send. Rate limiting is signalled in-band here; `/validate` never answers HTTP 429. `headers` and `cookies` are optional on every decision: apply each one you receive.

Content type `application/json`.

Schema:

```json
{
  "type": "object",
  "required": [
    "success",
    "decision"
  ],
  "properties": {
    "success": {
      "type": "boolean",
      "description": "`true` on a valid response.",
      "example": true
    },
    "decision": {
      "type": "string",
      "enum": [
        "allow",
        "block",
        "redirect"
      ],
      "description": "The validation decision. A `block` rendered from a custom block page is reported as `redirect`, so read `status_code` rather than inferring intent from this field."
    },
    "status_code": {
      "type": "integer",
      "enum": [
        200,
        302,
        401,
        403,
        404,
        418,
        429,
        451,
        460,
        503
      ],
      "description": "The HTTP status to return to the client. When present, send this value instead of a hardcoded status. It is 200 on standard `allow` responses and 403 on standard `block` and `redirect` responses unless a policy rule or tenant setting overrides it. Treat an absent value as an internal or intercept edge case, and apply your documented safe fallback.",
      "example": 403
    },
    "headers": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "description": "Optional. HTTP headers to set on your response. Any decision can carry them: set each one you receive. A `block` or `redirect` carries the headers of its page. A managed `robots.txt` is a `block` with `status_code` 200 and a `text/plain` `Content-Type`. Guard for a missing field before you iterate."
    },
    "response_html": {
      "type": "string",
      "description": "Base64-encoded UTF-8 response body. It can contain HTML or validator-owned plain text such as managed `robots.txt`. Read `headers.Content-Type` before you serve it. It usually exists on `block` and `redirect`. If a `redirect` lacks this field, deny the request with `status_code` and a safe fallback body. Do not pass the request to your application."
    },
    "cookies": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name",
          "value",
          "domain",
          "path",
          "secure",
          "same_site",
          "max_age"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "_centinel"
          },
          "value": {
            "type": "string",
            "example": "<session-value>"
          },
          "domain": {
            "type": "string",
            "example": "",
            "description": " Empty means host-only; do not substitute your own domain."
          },
          "path": {
            "type": "string",
            "example": "/"
          },
          "secure": {
            "type": "boolean",
            "description": "Set the `Secure` attribute.",
            "example": true
          },
          "same_site": {
            "type": "string",
            "description": "Value for the `SameSite` attribute.",
            "example": "Lax"
          },
          "max_age": {
            "type": "integer",
            "description": "Lifetime in seconds for the `Max-Age` attribute. Preserve this value to retain the validator-specified cookie persistence lifetime.",
            "example": 86400
          }
        }
      },
      "description": "Optional. Cookies to set on your response. Any decision can carry them: set each one you receive, with all its attributes. The validator sends `_centinel` when the request did not present the current session. It never sends `_centinel` to a matched crawler."
    },
    "crawler": {
      "type": "object",
      "required": [
        "id",
        "name",
        "access_allowed"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Crawler identifier (UUID).",
          "example": "4a7d7c8a-3fcb-4b91-8f7e-3f6ac1f5b6c1"
        },
        "name": {
          "type": "string",
          "description": "Human-readable crawler name.",
          "example": "Googlebot"
        },
        "access_allowed": {
          "type": "boolean",
          "description": "`true` only when the identified crawler source IP is verified and the crawler is allowlisted for your tenant. Use the top-level decision to enforce the validation result."
        },
        "category": {
          "type": "string",
          "description": "Catalog crawler type, copied unchanged. This is not the policy crawler.category value.",
          "example": "LLM"
        },
        "rsl_category": {
          "type": "string",
          "description": "Catalog RSL category, copied unchanged. It is independent of category and policy matching does not use it.",
          "example": "ai-all"
        }
      }
    },
    "block_reasons": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Identifiers of the signals that drove a `block`. Still populated in monitor mode, where the decision is reported as `allow`. Treat it as an open list of strings."
    },
    "block_reason_categories": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "bot_identity",
          "automated_browser",
          "header_anomaly",
          "fingerprint_mismatch",
          "suspicious_network",
          "challenge_failure",
          "crawler_impersonator",
          "unknown"
        ]
      },
      "description": "Higher-level grouping of `block_reasons`. One of `bot_identity`, `automated_browser`, `header_anomaly`, `fingerprint_mismatch`, `suspicious_network`, `challenge_failure`, `crawler_impersonator`, or `unknown`."
    },
    "country_code": {
      "type": "string",
      "description": "ISO 3166-1 alpha-2 country resolved for the client IP. Present only on `allow`, and only when country enrichment is enabled for your tenant."
    },
    "rate_limited": {
      "type": "boolean",
      "description": "`true` when the decision was driven by a rate-limit hit."
    },
    "retry_after": {
      "type": "integer",
      "description": "Seconds the client should wait before retrying when `rate_limited` is `true`."
    }
  }
}
```

Example `allow` (Allow):

```json
{
  "success": true,
  "decision": "allow",
  "status_code": 200,
  "cookies": [
    {
      "name": "_centinel",
      "value": "<session-uuid>",
      "domain": "",
      "path": "/",
      "secure": true,
      "same_site": "Lax",
      "max_age": 86400
    }
  ]
}
```

Example `allowWithCrawler` (Allow with crawler metadata):

```json
{
  "success": true,
  "decision": "allow",
  "crawler": {
    "id": "4a7d7c8a-3fcb-4b91-8f7e-3f6ac1f5b6c1",
    "name": "Googlebot",
    "access_allowed": true,
    "category": "LLM",
    "rsl_category": "ai-all"
  },
  "status_code": 200
}
```

Example `block` (Block):

```json
{
  "success": true,
  "decision": "block",
  "status_code": 403,
  "response_html": "PGh0bWw+PGJvZHk+QmxvY2tlZDwvYm9keT48L2h0bWw+",
  "headers": {
    "Content-Type": "text/html; charset=utf-8",
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY"
  },
  "cookies": [
    {
      "name": "_centinel",
      "value": "<session-uuid>",
      "domain": "",
      "path": "/",
      "secure": true,
      "same_site": "Lax",
      "max_age": 86400
    }
  ],
  "block_reasons": [
    "datacenter_ip"
  ],
  "block_reason_categories": [
    "suspicious_network"
  ]
}
```

Example `redirect` (Redirect (interstitial challenge)):

```json
{
  "success": true,
  "decision": "redirect",
  "status_code": 403,
  "response_html": "PGh0bWw+PGJvZHk+Q2hhbGxlbmdlPC9ib2R5PjwvaHRtbD4=",
  "cookies": [
    {
      "name": "_centinel",
      "value": "<session-uuid>",
      "domain": "",
      "path": "/",
      "secure": true,
      "same_site": "Lax",
      "max_age": 86400
    }
  ],
  "headers": {
    "Content-Type": "text/html; charset=utf-8",
    "Content-Security-Policy": "script-src 'nonce-<per-response>'",
    "X-Content-Type-Options": "nosniff"
  }
}
```

Example `rateLimited` (Rate limited (in-band on a 200)):

```json
{
  "success": true,
  "decision": "block",
  "status_code": 403,
  "response_html": "PGh0bWw+PGJvZHk+QmxvY2tlZDwvYm9keT48L2h0bWw+",
  "headers": {
    "Content-Type": "text/html; charset=utf-8",
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY"
  },
  "cookies": [
    {
      "name": "_centinel",
      "value": "<session-uuid>",
      "domain": "",
      "path": "/",
      "secure": true,
      "same_site": "Lax",
      "max_age": 86400
    }
  ],
  "block_reasons": [
    "rate_limit_exceeded"
  ],
  "block_reason_categories": [
    "suspicious_network"
  ],
  "rate_limited": true,
  "retry_after": 30
}
```

Example `robotsTxt` (Managed robots.txt (block with status 200)):

```json
{
  "success": true,
  "decision": "block",
  "status_code": 200,
  "response_html": "VXNlci1hZ2VudDogKgpBbGxvdzogLwo=",
  "headers": {
    "Content-Type": "text/plain; charset=utf-8"
  }
}
```

#### 400

Request rejected. Fail closed on this status.

Content type `application/json`.

Schema:

```json
{
  "type": "object",
  "required": [
    "success",
    "error"
  ],
  "properties": {
    "success": {
      "type": "boolean",
      "example": false
    },
    "error": {
      "type": "object",
      "required": [
        "type",
        "message"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "Stable error identifier (e.g., `invalid_request`, `invalid_api_key`).",
          "example": "invalid_request"
        },
        "message": {
          "type": "string",
          "description": "Human-readable error description."
        }
      }
    }
  }
}
```

Example:

```json
{
  "success": false,
  "error": {
    "type": "missing_fields",
    "message": "Missing required fields: URL and IP are required"
  }
}
```

#### 401

Missing or invalid `x-api-key` header, as `missing_api_key` or `invalid_api_key`. Fail closed on this status.

Content type `application/json`.

Schema:

```json
{
  "type": "object",
  "required": [
    "success",
    "error"
  ],
  "properties": {
    "success": {
      "type": "boolean",
      "example": false
    },
    "error": {
      "type": "object",
      "required": [
        "type",
        "message"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "Stable error identifier (e.g., `invalid_request`, `invalid_api_key`).",
          "example": "invalid_request"
        },
        "message": {
          "type": "string",
          "description": "Human-readable error description."
        }
      }
    }
  }
}
```

Example:

```json
{
  "success": false,
  "error": {
    "type": "invalid_api_key",
    "message": "Invalid API key"
  }
}
```

#### 500

Validator error. Fail open on this status so your site stays available.

Content type `application/json`.

Schema:

```json
{
  "type": "object",
  "required": [
    "success",
    "error"
  ],
  "properties": {
    "success": {
      "type": "boolean",
      "example": false
    },
    "error": {
      "type": "object",
      "required": [
        "type",
        "message"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "Stable error identifier (e.g., `invalid_request`, `invalid_api_key`).",
          "example": "invalid_request"
        },
        "message": {
          "type": "string",
          "description": "Human-readable error description."
        }
      }
    }
  }
}
```

Example:

```json
{
  "success": false,
  "error": {
    "type": "internal_server_error",
    "message": "Internal server error"
  }
}
```

#### 503

The validation pipeline exceeded its time budget. Fail open on this status.

Content type `application/json`.

Schema:

```json
{
  "type": "object",
  "required": [
    "success",
    "error"
  ],
  "properties": {
    "success": {
      "type": "boolean",
      "example": false
    },
    "error": {
      "type": "object",
      "required": [
        "type",
        "message"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "Stable error identifier (e.g., `invalid_request`, `invalid_api_key`).",
          "example": "invalid_request"
        },
        "message": {
          "type": "string",
          "description": "Human-readable error description."
        }
      }
    }
  }
}
```

Example:

```json
{
  "success": false,
  "error": {
    "type": "validation_timeout",
    "message": "Validation process timed out, please retry"
  }
}
```

The full OpenAPI document is at https://docs.centinelanalytica.com/openapi.json.
