The OpenAPI 3.1 description and JSON Schemas of the ShieldLabs API: History API, Management API, Health and webhooks.
- Browser. The ShieldLabs agent runs an identification and hands your page a request ID. The browser never receives a Risk Score, a visitor ID or a device ID.
- Your backend. It sends the request ID along with the protected action (signup, login, checkout) and reads the verdict from the History API, or receives it in a signed
identification.scoredwebhook. - Decision. It acts on the Risk Score, the three risk bands, the detection flags and the identifiers, for example how many accounts one device has used.
This repository describes steps 2 and 3 exactly as they happen on the wire: every public endpoint, both webhook events, the error bodies the servers really send, and examples taken from the shared test fixtures of the official SDKs.
npm install @shieldlabs-ai/openapiUse npm install --save-dev @shieldlabs-ai/openapi instead when you only generate code or validate payloads in tests. A webhook handler that validates events with the JSON Schemas at runtime needs the package as a regular dependency.
Or download the files you need:
| File | Contents |
|---|---|
dist/shieldlabs-api.yaml |
The bundled OpenAPI 3.1 document |
dist/shieldlabs-api.json |
The same document as JSON |
dist/schemas/webhook-event.schema.json |
Any webhook delivery (unknown event types pass by their envelope) |
dist/schemas/identification-scored-event.schema.json |
The identification.scored event |
dist/schemas/webhook-ping-event.schema.json |
The webhook.ping event |
dist/schemas/history-page.schema.json |
A History API page ({"data": [...], "total": N}) |
dist/schemas/history-row.schema.json |
One History API row |
dist/schemas/identification.schema.json |
The normalized Identification model the server SDKs return |
Stable download URLs:
https://cdn.jsdelivr.net/npm/@shieldlabs-ai/openapi@1/dist/shieldlabs-api.yaml(any file of the package, latest 1.x)https://raw.githubusercontent.com/ShieldLabs-ai/shieldlabs-openapi/main/dist/shieldlabs-api.yaml- every GitHub release attaches the
dist/files
Load the document in Node.js 20.10 or later: save this as index.mjs next to your node_modules and run node index.mjs.
import spec from '@shieldlabs-ai/openapi' with { type: 'json' };
// CommonJS: const spec = require('@shieldlabs-ai/openapi');
console.log(spec.info.version); // "1.0.1"
console.log(Object.keys(spec.paths)); // [ '/api/v1/history/{search_type}/{value}', ... ]Read one verdict with curl:
curl "https://account.shieldlabs.ai/api/v1/history/request_id/a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d?limit=1" \
-H "Authorization: Bearer $SHIELDLABS_API_KEY"Open the rendered reference: serve the repository root and visit /docs/.
python3 -m http.server 8080
# http://localhost:8080/docs/The server SDKs wrap this contract with typed models, History polling with backoff, webhook verification and risk helpers: @shieldlabs-ai/node, shieldlabs for Python, shieldlabs-go, shieldlabs/shieldlabs-php, ai.shieldlabs:shieldlabs-java and ShieldLabs for .NET. Use this repository directly when you generate a client for another language, validate payloads in your own tests, or render the reference.
Any generator that reads OpenAPI 3.1 can use dist/shieldlabs-api.yaml. For example, TypeScript types for every path, operation and schema:
npx openapi-typescript@7 node_modules/@shieldlabs-ai/openapi/dist/shieldlabs-api.yaml -o shieldlabs-api.d.tsKeep these points in mind with any generator:
- Two hosts. Every operation declares its own server: the History API on
https://account.shieldlabs.ai, the Management API onhttps://api.shieldlabs.ai. Some generators only use the first root server; configure the base URL per API yourself in that case, and never give the History API a base URL ending in/api(the paths already start with/api/v1/). - Validate before sending. The History API does not validate its path. An unknown
search_typereturns the latest identifications of the whole domain, and a malformed UUID or IPv4 value returns500. Restrictsearch_typeto the seven values and checkvaluefirst. - User HID in the path. The History API matches a User HID only when the path uses one exact encoding:
A-Z a-z 0-9 - . _ ~ $ & + , : ; = @unescaped, and every other byte of the UTF-8 value as uppercase%XX(including! ' ( ) *). Any other encoding is compared literally and returns an empty page. Many generated clients escape$ & + , : ; = @in path values, so build this path yourself for User HIDs that contain them. A User HID containing/, and the values.and.., cannot be searched. Hex-encoded hashes need no escaping. - Management credentials. Send
X-Shield-Domain(the registered domain, lowercase, without scheme or a leadingwww.) together with the Secret Key. - Open sets. Signal names, operating systems, browsers, connection types, device types and the traffic fields are open sets: the descriptions list the known values (so does
x-extensible-enumwhere the list is fixed today), and new values can appear. Parse unknown values instead of failing.
Install the package and the validator as regular dependencies:
npm install @shieldlabs-ai/openapi ajv ajv-formatsVerify the signature over the raw body first, then parse, then validate (an ES module, for example shieldlabs-webhook.mjs):
import { createHmac, timingSafeEqual } from 'node:crypto';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import schema from '@shieldlabs-ai/openapi/schemas/webhook-event.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true });
addFormats(ajv);
const validateEvent = ajv.compile(schema);
// rawBody: the exact bytes received (Buffer); secrets: current and, during a rotation, previous secret.
export function verifySignature(rawBody, header, secrets) {
const match = String(header ?? '').trim().match(/^sha256=([0-9a-fA-F]{64})$/);
if (!match) return false;
const received = Buffer.from(match[1].toLowerCase(), 'hex');
return secrets.some((secret) => {
if (!secret) return false;
const expected = createHmac('sha256', Buffer.from(secret, 'utf8')).update(rawBody).digest();
return timingSafeEqual(expected, received);
});
}
export function parseEvent(rawBody) {
const event = JSON.parse(rawBody.toString('utf8'));
if (!validateEvent(event)) console.warn('ShieldLabs event does not match the schema', validateEvent.errors);
return event;
}- The key is the whole signing secret,
whsec_prefix included, as UTF-8 bytes. The message is the raw body: re-serialized JSON does not match (for example,&arrives as\u0026). - Delivery attempts have a 1-second timeout. Network errors, timeout, 429 and 5xx retry with backoff within a bounded window (8 failed sends or 15-minute retry age); worker/Redis waits can extend wall time. Retries preserve signed bytes and
event_id, and duplicate deliveries remain possible. Verify the raw-body signature, durably store by signedevent_id(legacy fallback:data.request_id), then acknowledge 2xx and process asynchronously. Redirects are not followed. - Current Portal Test sends a complete
2026-10-07Core-generated sample with HRE cluster references and a distinct event ID per click. Vendored historical Test fixtures retain their two missing flags as explicit legacy compatibility cases. - A History row can be refined after its webhook was sent; the webhook is not sent again. Read the History API when you need the latest state.
- Search by
request_idwithlimit=1right after a protected action. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up checks finish, so start the identification when the user begins the action (for example when the signup form opens). An emptydataarray means "not scored yet", never "clean": poll with backoff for up to about 10 seconds. - Search by
device_id,user_hid,visitor_idoripfor account-level checks. When you count accounts, skip rows whoseuser_hidis empty or one ofanonymous,fail,-1andunknown: these do not identify a user. - Page with
offsetwhile it is belowtotal, and deduplicate onrequest_id. score_detailsis a JSON-encoded string;created_atisYYYY-MM-DD HH:MM:SS.mmmin UTC.
SHIELDLABS_API_KEY=sec_your_private_key npm run check:liveThe script searches for a random request ID and expects {"data":[],"total":0}. Optional variables: SHIELDLABS_LIVE_REQUEST_ID (validates that row), SHIELDLABS_SECRET_KEY with SHIELDLABS_DOMAIN (one Management API profile call), SHIELDLABS_API_BASE_URL and SHIELDLABS_MANAGEMENT_BASE_URL (https only; plain http is accepted for localhost, 127.0.0.1 and [::1]). It never prints keys and is skipped when SHIELDLABS_API_KEY is not set.
| operationId | Request | Credentials | Purpose |
|---|---|---|---|
searchHistory |
GET https://account.shieldlabs.ai/api/v1/history/{search_type}/{value} |
Private API Key | Identifications matching one identifier, newest first |
getDomainProfile |
GET https://api.shieldlabs.ai/v1/profile |
Secret Key + X-Shield-Domain |
Domain, remaining included identifications, masked keys |
searchHistoryDeprecated |
GET https://api.shieldlabs.ai/v1/history/{type}/{value} |
Secret Key + X-Shield-Domain |
Deprecated, stops working after Sat, 01 Jan 2027 00:00:00 GMT |
getHealth |
GET /health on both hosts |
none | Liveness |
| Event | operationId | When |
|---|---|---|
identification.scored |
identificationScored |
Once per identification and endpoint, when scoring is final |
webhook.ping |
webhookPing |
When you verify an endpoint in the analytics dashboard |
Header: X-Shield-Signature: sha256=<lowercase hex HMAC-SHA256(key = signing secret, message = raw body)>.
| Import | File |
|---|---|
@shieldlabs-ai/openapi |
dist/shieldlabs-api.json |
@shieldlabs-ai/openapi/shieldlabs-api.json, @shieldlabs-ai/openapi/shieldlabs-api.yaml |
the bundle |
@shieldlabs-ai/openapi/schemas/<name>.schema.json |
dist/schemas/<name>.schema.json |
@shieldlabs-ai/openapi/spec/... |
the split sources |
The Risk Score is an integer from 0 to 100; bands are computed on your side: trusted 0-29, suspicious 30-59, dangerous 60-100. A value above 100 is the rate-limit marker (999), never a score. The marker is written once, when the visitor's IP goes over the limit; request IDs issued while that IP stays blocked get no row at all, so treat them as unverified. Branch on the detection flags and the score; risk signal names are for display and logging, and their weights can be negative, so never add them up yourself.
| API | Status | Body | Retry |
|---|---|---|---|
| History | 401 | JSON text {"error":"..."} sent as text/plain |
no |
| History | 429 | {"error":"too many requests"} (about 15 requests per second per domain) |
yes, after about a second |
| History | 500 | {"error":"..."} as JSON, or JSON text for a failed key lookup |
yes, unless caused by a malformed UUID or IPv4 value |
| Management | 400 | a bare JSON string or null (deprecated history endpoint) |
no |
| Management | 401 | empty | no |
| Management | 429 | {"error":"too many requests"} (15 requests per minute per IP, then a 10-minute block) |
no: wait, and cache the profile |
| Management | 503 | {"error":"server is busy"} |
yes, with backoff |
| Both | 404 | 404 page not found as text/plain |
no |
| Both | 502, 504 | HTML from the edge proxy | yes, with backoff |
Branch on the status first, then try to parse the body as JSON whatever its content type.
When present, client_identity contains the stored ingest attribution. claims
are observed names, while each verified entry proves only its named subject
(provider, service or infrastructure) through referenced evidence. A verified
provider does not verify the claimed agent name or AI mode, and attribution does
not change the Risk Score or detection flags. An absent object means no stored
identity data. Older SDK models can retain this additive field in their raw
History row or webhook data. Companion SDK changes add typed optional fields in
Node, Go, Python, PHP, Java and .NET; use a version containing those changes to
access the typed field.
- OpenAPI 3.1.0; the JSON Schemas use JSON Schema 2020-12 and compile with a strict validator.
info.versionfollows the package version (semantic versioning). Webhook payloads carryschema_version(2026-10-07current;2026-10-06and2026-06-01legacy).- Parse tolerantly: ignore unknown fields, keep unknown values of string fields and accept other
schema_versionvalues. Response fields with a known set of values are open strings in the spec and in the JSON Schemas, so a value added later never fails validation. - Tooling requires Node.js 20.19 or later; CI runs on Node.js 20, 22 and 24.
npm ci
npm run lint # Redocly, strict configuration
npm test # bundle freshness, examples, fixtures, exports, tooling
npm run bundle # rebuild dist/ after editing spec/Runnable scripts that use the published files live in examples/: listing
every operation with its server, and verifying plus validating a webhook delivery. See
CONTRIBUTING.md for the layout of spec/ and how to add examples.
Documentation: https://docs.shieldlabs.ai · Analytics dashboard (Start free): https://app.shieldlabs.ai · Support: contact@shieldlabs.ai
MIT © 2026 ShieldLabs Inc.
#202 contract and fixtures prepare the next major release. They are tested separately from the deployed 1.x schema; physical retained-read and release gates remain open.
Endpoints may opt in with multiaccount: true (default false). The new signed
hre.multi_account.changed body describes the whole group, not an originating
request. detected and updated include the full active group; resolved includes
its previous member set and detected=false, level=null. A changed member set has a
new cluster ID. Ignore older numeric revisions per site/epoch/group and deduplicate
event_id atomically with your business action before acknowledging 2xx.
The standalone schema is exported as
@shieldlabs-ai/openapi/schemas/multiaccount-changed-event.schema.json after release.
Existing SDK signature helpers preserve raw-byte verification and their unknown-event
parsers can carry this event; consumers must add their group-event handler before
opting in. The package is not published by this PR. Core emission stays off until
its trusted original group-set proof and operational budgets pass their release gates.