> ## Documentation Index
> Fetch the complete documentation index at: https://docs.botshield.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Result states

> Every BotShield Gate verification resolves to one of two result states, Verified or Unavailable, and your integration should be designed around both.

A BotShield Gate verification has exactly two outcomes. Either a real human confirmed they are present, or BotShield could not confirm that right now. There is no score, no confidence value, and no "bot" verdict.

## The two states

| Result state | Wire value | Meaning |
| - | - | - |
| **Verified** | `human_verified` | A real human is present. They confirmed with their device biometric, or they are a returning human on a Recent Presence gate. |
| **Unavailable** | `unavailable` | BotShield cannot return a verified result for this request. Nothing more is implied. |

You see the same two states on every surface:

| Surface | Verified | Unavailable |
| - | - | - |
| Widget events | `botshield:success` | `botshield:failure` with a `reason` |
| `botshield:census-status` event | `result_state: "human_verified"` | `result_state: "unavailable"` |
| Webhooks | `gate.human_verified` | `gate.unavailable` |
| `GET /verification/status` | `status: "completed"` or `"pass"` | `status: "failed"` or `"expired"` |

(`census` in the event name is the API's name for BotShield Gate.)

The `botshield:census-status` event fires after the widget's first check. On a Human Gate, `unavailable` at that point usually means "a confirmation on the phone is needed", and the widget continues to the QR code or deep link. Wait for `botshield:success` or `botshield:failure` before you decide anything.

## Why there is no score

A score asks you to pick a threshold and to accept that some humans fall under it. BotShield does not estimate how human a session looks. It asks a human to confirm on their own device, and reports whether that happened.

That is also why there is no "bot" verdict. BotShield can observe that a human confirmed. It cannot observe that a human is absent. A person who declines, walks away, or has an unsupported phone looks the same as an automated client that never finishes. BotShield reports **Unavailable** in all of those cases and leaves the policy decision to you.

## What Unavailable covers

A `gate.unavailable` webhook comes in two shapes.

**A failure** carries `failed_at` and a `reason`. It is sent when the BotShield app reports that the confirmation did not succeed, and when a request that was opened fails on BotShield's side.

| `reason` | What happened |
| - | - |
| `user_denied` | The human cancelled or declined the confirmation, for example by dismissing the biometric prompt. |
| `device_lock_required` | The human's phone reported no passcode, screen lock, or biometric, so it cannot produce a confirmation. See [Device security](/concepts/device-security). |
| `platform_declined` | The phone's platform refused the confirmation, because it does not support it or blocked it for a security reason. |
| `internal_error` | Anything else. This is the default, and BotShield sends it whenever it has no more specific reason. |

```json theme={null}
{
  "type": "gate.unavailable",
  "event_id": "req_3f9a1c…",
  "request_id": "req_3f9a1c…",
  "failed_at": "2026-09-21T17:05:40.000Z",
  "reason": "user_denied",
  "error_message": "User cancelled passkey verification",
  "environment": "development",
  "product": "census",
  "metadata": { "booking_ref": "MA-20418" }
}
```

`environment` tells you which environment the request ran in. `metadata` holds only the keys you supplied when you created the request, and is left out when you supplied none. See [Webhook events](/webhooks/events).

**An expiry** carries `expired_at` and no `reason`. A verification request expires five minutes after it is created. The expiry event is sent when the human opens the request after that point.

`reason` is there to help you choose the right message: a person who cancelled needs a retry button, and a phone with no screen lock needs a line of help. All four values are still the same result state, Unavailable.

<Warning>
  Do not wait for a `gate.unavailable` webhook before you let the user move on. A request that the human never opens on their phone expires without sending any webhook. Treat any request with no Verified result by its `expires_at` as Unavailable.
</Warning>

The widget reports its own `reason` on `botshield:failure`. `expired` and `failed` are user outcomes. `origin_not_allowed` (your page's origin is not on the site key's list), `invalid_site_key` (the site key is unknown or revoked), and `gate_not_found` (the key's environment has no Active gate with that gate key) are configuration errors on your side. The separate `botshield:cancel` event, with `request_id` in its detail, fires when the person closes the widget's modal before finishing. The widget goes back to idle, and no result state is reported, because the person can start again. See [Web component](/gate/web-component) for the full list.

## The verification status enum

`GET /verification/status?request_id=…` returns a `status` field. It needs no API key, and it returns its payload directly under `data`.

| `status` | Meaning | Result state |
| - | - | - |
| `pending` | The request exists and the human has not finished. | None yet |
| `completed` | The human confirmed on their phone. `verified_at` and `verification_token` are set. | Verified |
| `pass` | A recognized returning human passed a Recent Presence gate instantly. There is no token. The record reads `expired` 60 seconds after the check. | Verified |
| `failed` | The confirmation did not succeed. `error_message` may say why. | Unavailable |
| `expired` | The request passed its `expires_at` without completing. | Unavailable |
| `not_found` | No request has this `request_id`. `found` is `false`. | None |
| `error` | The lookup itself failed. Retry. | None |

```json theme={null}
{
  "data": {
    "found": true,
    "status": "completed",
    "request_id": "req_3f9a1c…",
    "organization_id": "org_…",
    "created_at": "2026-09-21T17:03:52.000Z",
    "expires_at": "2026-09-21T17:08:52.000Z",
    "verified_at": "2026-09-21T17:04:11.000Z",
    "verification_token": "eyJhbGciOiJFUzI1NiIs…",
    "signed_token": null,
    "error_message": null,
    "sdk_type": "signal",
    "scope": "checkout",
    "presence_address": null,
    "gate_type": "human",
    "age_threshold": null,
    "age_verdict": null,
    "age_source": null,
    "metadata": {
      "scope": "checkout",
      "sdk_type": "signal",
      "environment": "development",
      "return_url": null,
      "parent_request_id": "req_8d02…"
    }
  }
}
```

<Warning>
  Never test for `status === "completed"` alone. Accept `completed` or `pass` as Verified. Look up an instant pass as soon as your page reports it. If the record already reads `expired`, ask the user to verify again. See the [Quick start](/quick-start).
</Warning>

<Note>
  On an Age Gate (Beta), `status: "completed"` means a human confirmed. It does not mean the age threshold was met. Read `age_verdict` as well. See [Age Gate](/gate/age-gate).
</Note>

## Design for Unavailable

Unavailable is an ordinary outcome for real people. Phones run out of battery, people change their minds, and some people do not have the BotShield app yet. Offer, never punish.

* **Offer a way forward.** Let the user try again, or route them to the path you already had: a waiting room, your existing challenge, manual review, or a later attempt.
* **Do not label the user.** Avoid copy such as "bot detected" or "verification failed, access denied". Prefer "We couldn't confirm you just now."
* **Do not penalize the account.** Do not lock, flag, or downgrade a user because a verification was Unavailable.
* **Reward Verified instead.** Give verified humans the better path: earlier access, a skipped challenge, a reserved allocation. The gate then pulls people in rather than pushing them out.
* **Keep retries cheap.** A new attempt creates a new request. Nothing carries over from the Unavailable one.

<CardGroup cols={2}>
  <Card title="BotShield ID" icon="id-badge" href="/concepts/botshield-id">
    Why returning humans pass Recent Presence gates instantly.
  </Card>

  <Card title="Webhook events" icon="bell" href="/webhooks/events">
    Full payloads for `gate.human_verified` and `gate.unavailable`.
  </Card>

  <Card title="Web component" icon="code" href="/gate/web-component">
    Widget events and failure reasons.
  </Card>

  <Card title="Device security" icon="lock" href="/concepts/device-security">
    What happens when the user's phone has no screen lock.
  </Card>
</CardGroup>
