---
title: "How to read a Signup Protection result | Emvero"
description: "Read the combined action, email result and visit findings. Understand token reuse, informational VPN findings, unavailable signals and pending email."
url: https://emvero.io/blog/reading-a-signup-check/
language: en
updated: 2026-10-30
---

Blog

# How to read a signup check

A signup check combines the email decision with evidence about the visit. Read the overall action, then inspect the two parts to understand what changed it.

Published 2026-10-30

Use the response’s outer `action` to decide how registration should proceed. A deliverable email address can still require confirmation because of the visit signals. A signal that requires confirmation does not establish that the visitor is a bot.

The examples below describe the combined response from `POST /v1/signup-checks`. They are synthetic, shortened illustrations of selected fields, not complete API responses or measurements of real visitors.

## Read the response in this order

1. Read the outer `action`: `allow`, `allow_with_email_confirmation` or `reject`. This recommendation accounts for both parts of the check.
2. Read `email.action` and `email.status` to understand the email decision. The email’s `reason_codes` explain its evidence and limits.
3. Read `signals.status` to see whether visit signals were evaluated, the token was unusable or the service was unavailable.
4. Inspect `signals.findings` and `signal_codes` to see which findings required confirmation. Check `not_evaluated` and `not_evaluated_classes`, when present, for comparisons that could not be made.

These fields answer different questions. `evaluated` describes the state of the signal assessment; it is not a successful signup verdict. An empty `signal_codes` list means no finding raised the recommendation, not that every comparison ran or the visit was proved safe.

If you use `POST /v1/signup-signals` instead, the response has no email part. Its `action` is the signals-only recommendation, as though email had returned `allow`. It cannot replace the email policy you apply elsewhere.

## A deliverable email can still require confirmation

In this example, the email result allows registration. The same token has been presented in a new operation, so a signal asks for confirmation:

```
{
  "action": "allow_with_email_confirmation",
  "signal_codes": ["fp_token_reused"],
  "email": {"status": "deliverable", "action": "allow"},
  "signals": {
    "status": "evaluated",
    "findings": [
      {"code": "fp_token_reused", "class": "token", "default_raises": true}
    ]
  }
}
```

Reading only `email.action` would miss the confirmation requirement. The outer `action` includes it. The finding records token reuse; it does not establish why the token was used again or who submitted the request.

Your application implements `allow_with_email_confirmation`: send a confirmation message and check its link or code before granting the access that depends on mailbox control. Emvero’s email verification sends no message to the address being checked. The distinction between [email verification and email confirmation](https://emvero.io/blog/email-verification-vs-email-confirmation/) matters here: confirmation establishes access to a mailbox, not a unique human identity.

Signals can move an email decision from `allow` to `allow_with_email_confirmation`. They cannot cause `reject`, and they cannot soften an email rejection or remove an existing confirmation requirement. When the outer action is `reject`, its basis is the email decision.

## Find which evidence changed the action

`signals.findings` contains the reported findings. Each finding’s `default_raises` says whether it asks for confirmation under Emvero’s default policy. The top-level `signal_codes` contains the codes of findings with `default_raises: true`, in their returned order and without duplicates.

A finding can therefore appear without changing the action. This VPN example keeps the email decision:

```
{
  "action": "allow",
  "signal_codes": [],
  "email": {"status": "deliverable", "action": "allow"},
  "signals": {
    "status": "evaluated",
    "findings": [
      {"code": "fp_network_vpn", "class": "identity", "default_raises": false}
    ]
  }
}
```

`fp_network_vpn` reports a network observation. With `default_raises: false`, that finding alone does not require confirmation. VPN, Tor, proxy and hosting findings are informational by default; a network label does not establish abuse. Read the returned flag rather than guessing from a code’s name. The [signal-code reference](https://docs.emvero.io/answers/signal-codes/) defines the vocabulary.

The email’s `reason_codes` stay inside `email`. They explain email evidence and do not become signal codes. The [email-result article](https://emvero.io/blog/reading-a-verification-result/) covers that part in detail. Keep the two code lists separate when displaying a result or investigating a signup.

`signal_codes` can be non-empty even when email already requires confirmation or rejection. The list records the signals that raised their part of the recommendation; it does not tell you that signals were the only cause of the final action.

## Browser findings and notes have different roles

`signals.probe` reports whether the browser probe was read and which of its checks could run. `fp_probe_forged` and `fp_probe_automation` have `default_raises: true`; they can require email confirmation. `fp_probe_tampering` is informational because browser extensions and page code can also alter native properties.

The separate `signals.notes` array carries `rtt_extra_leg`, `webtransport`, `ua_brand`, `bot_claim` and `bot_operator`. `notes_version` identifies the vocabulary; the current version is 2. Notes do not enter `signal_codes` or change the default action. An unavailable note or a missing probe limits the evidence; it is not a failed token or a signals-service outage. Each note includes a `value`, `rules_version` and `group`. Your application can consider notes alongside its own history; observations in the same group may describe one cause and should not be treated as independent proof.

`bot_claim` reports a declared crawler whose network matches its operator (`crawler_verified`), a claim outside that network (`crawler_unverified`), a crawler with no supported operator check (`crawler_unchecked`), a declared tool (`tool`) or no such claim (`none`). `bot_operator` names the declared crawler’s operator. `unavailable` means the note could not be evaluated; `none` does not establish a human visitor. Even `crawler_unverified` is informational. Repeat-visit counts, farm-cluster detection and a raising signal for declared tools are not currently provided.

A missing browser probe can produce `fp_probe_not_evaluated` while the token remains valid and `signals.status` is `evaluated`. That operation still costs five signal credits. A note value of `unavailable` or `unknown` limits what the note establishes; it does not mean that the whole signal service was unavailable. The [signal-code reference](https://docs.emvero.io/answers/signal-codes/) explains the new findings and note vocabulary.

## Compare the combined outcomes

These ten synthetic cases use the same result contract. The ordinary case has no reported findings. The VPN case has an informational finding and no raising code.

| Case | `email.status` / `email.action` | `signals.status` | `signal_codes` | Outer `action` |
| --- | --- | --- | --- | --- |
| Ordinary signup | `deliverable` / `allow` | `evaluated` | `[]` | `allow` |
| Token used in a new operation | `deliverable` / `allow` | `evaluated` | `["fp_token_reused"]` | `allow_with_email_confirmation` |
| Token absent | `deliverable` / `allow` | `token_unusable` | `["fp_token_absent"]` | `allow_with_email_confirmation` |
| Informational VPN finding | `deliverable` / `allow` | `evaluated` | `[]` | `allow` |
| Signals service unavailable | `deliverable` / `allow` | `unavailable` | `[]` | `allow` |
| Email still being checked | `pending` / `allow_with_email_confirmation` | `evaluated` | `[]` | `allow_with_email_confirmation` |
| Email rejected | `undeliverable` / `reject` | `evaluated` | `[]` | `reject` |
| Browser automation finding | `deliverable` / `allow` | `evaluated` | `["fp_probe_automation"]` | `allow_with_email_confirmation` |
| Informational tampering and notes | `deliverable` / `allow` | `evaluated` | `[]` | `allow` |
| Browser probe not evaluated; valid token | `deliverable` / `allow` | `evaluated` | `[]` | `allow` |

The two `allow` outcomes for evaluated and unavailable signals do not establish the same thing. In the ordinary case, signals were evaluated and none raised the action. In the unavailable case, there was no completed signal assessment; email alone determined the recommendation.

## Separate an unusable token from unavailable signals

An absent token gives `signals.status: token_unusable`, with `fp_token_absent` in `signal_codes`. An expired token also gives `token_unusable`, with `fp_token_expired`. These findings require confirmation when email would otherwise allow. They do not require you to treat the visitor as abusive: a blocked script or a long-running form can leave an ordinary visitor without a usable token.

When the signals service cannot evaluate the request, the combined endpoint can return HTTP `200` with `signals.status: unavailable`. In that outcome, the `signals` object contains only its status:

```
{
  "action": "allow",
  "signal_codes": [],
  "email": {"status": "deliverable", "action": "allow"},
  "signals": {"status": "unavailable"}
}
```

The service has not established that the visit is safe or that no VPN is present. The outer action follows email alone. Your application decides what access it permits while signal evidence is unavailable. If email required confirmation or rejection, that requirement would still stand.

Even an `evaluated` result can have gaps. A missing IP address, missing browser information or unavailable measurement can prevent individual comparisons. Where returned, `not_evaluated` and `not_evaluated_classes` describe those gaps. They are not extra email verdicts, and absence of a finding does not prove that its comparison passed.

An HTTP error that prevents you receiving a signup result is a separate integration outcome. Do not turn it into a made-up `evaluated` result. The [API response contract](https://docs.emvero.io/using/signup-protection/) distinguishes signal outcomes from request, connection and accounting errors.

## A pending email does not mean pending signals

When `email.status` is `pending`, the signals are already final. The outer `action` remains `allow_with_email_confirmation` while the email check finishes. Preserve the check ID and retrieve `GET /v1/signup-checks/{id}` after `email.retry_after_ms`.

That read returns the current email result with the stored signals and recalculates the combined action. It does not assess the visit again. If email later returns `allow`, a stored raising signal still requires confirmation; if email returns `reject`, the combined action becomes `reject`.

`pending` is unfinished email work. A final `unknown` email result means the evidence did not settle the address. Neither state establishes that the address is invalid. Follow the returned action and use the email’s evidence to understand the limitation.

## Replay the operation without creating a token-reuse event

If a response is lost, retry the same logical operation with its saved `Idempotency-Key` and the same request body. Within the 30-minute replay window, the API returns the saved check. The replay does not reevaluate the stored signals or create a second charge for the request. In a combined check, that saved record can include a newer email result; replay is not a promise that every response byte stays frozen while email finishes.

A new operation with the same token is a second presentation. It can add `fp_token_reused` and require confirmation, as in the first example. Generating a fresh key after a lost response changes the meaning of the request. Use the original key for a transport retry and a new token for a genuinely new signup attempt.

Reusing a key with a different body gives `409 idempotency_conflict`. If the first request is still running, `409 request_in_progress` tells you to wait according to `Retry-After`. The [idempotency documentation](https://docs.emvero.io/using/idempotency/) explains the operation boundary. Reading the stored check with `GET` is also distinct from submitting a new check with the same token.

To connect these decisions to your own registration flow, follow the [guide to stopping bot registrations](https://emvero.io/guides/stop-bot-registrations/). The [Signup Protection page](https://emvero.io/signup-protection/) explains the product and the path to connecting your form and server.

## Situations this applies to

### Stop bot registrations

Protect the account-creation endpoint, check the visit behind a submission and require mailbox confirmation when the result calls for it.

[All articles](https://emvero.io/blog/)

## Connect Signup Protection to your registration form.

Create an account, set up a page key and add the script and server call. Use the returned action in your application.

Start for free

View documentation
