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

# Evaluate a flow against your rules

> **Beta.** The request and response shapes may still change. Talk to us before you build against it. Flows, recipes and rules are authored through the Watch Management API, or configured by Prelude on your behalf.

Score a target against the rules configured for one moment in your product — signup, checkout, password reset. The flow selects which recipes run; each recipe scores its rules against a threshold and returns its own verdict, and the evaluation answers with the most severe verdict across them. The verdict is advisory: what to do about it is yours. Where Predict returns a single model-derived outcome, Eval returns the full breakdown, so you can see which rules fired and which could not run.

<Note>
  **Eval is in beta.** The request and response shapes may still change. Talk to us before you build against it.
</Note>

Flows, recipes and rules are yours to author with the [Management API](/watch/v2/api-reference/management/flows/create-flow) - see [Rules, recipes and flows](/watch/v2/documentation/rules-recipes-flows) for the model. Prelude can also configure them on your behalf.

Call **Eval** at any moment you want to guard: signup, checkout, password reset, a payout request. Set `flow_id` to the flow configured for that moment, and `target` to the end user you are evaluating - `phone_number`, `email_address`, or both. At least one is required; send both when you hold both, since a rule may read either.

A **flow** selects which **recipes** run. Each recipe scores its **rules** against a threshold and returns its own verdict. The evaluation answers with the most severe verdict across all of them.

Eval is **scoring-only** and **advisory**. It doesn't update long-lived abuse counters by itself, and the verdict is a recommendation: enforcement is yours.

## Eval compared with Predict

Both score an identifier. They differ in what they return and what you control.

| | [Predict](/watch/v2/api-reference/predict-outcome) | Eval |
| - | - | - |
| Answer | One outcome: `legitimate` or `suspicious` | Per-recipe verdicts with a per-rule breakdown |
| Logic | Prelude's model | Recipes and rules you configure |
| You see | Risk factor categories | Which rule fired, its weight, the score against the threshold |
| Guards | Signup | Any moment you define a flow for |

Use Predict when you want an answer. Use Eval when you want to see the reasoning, tune it, or add conditions of your own.

## Reading the response

Start at `recipes[].verdict` and work down. A recipe flags when its `score` reaches its `threshold`. An overriding rule can settle the verdict first, and that case sets `determined_by`. It's the only thing that accounts for a recipe reporting a score under its threshold and flagging anyway.

`partial_evidence` means at least one rule couldn't run, so the score rests on less than the whole recipe. Each such rule reports `outcome: NOT_EVALUATED` with a `blocked_by` naming what it waited on. This isn't a quieter `NOT_TRIGGERED`: the rule contributed nothing in either direction. A `PASS` carrying `partial_evidence` is weaker evidence than a `PASS` without it.

The usual cause is a signal that never arrived. Passing `dispatch_id` from the front-end SDK fills in most of what rules read. Without it, any rule reading a device or network signal reports `NOT_EVALUATED`.

`SKIPPED` is different. Once an overriding rule has determined a recipe's verdict, a remaining rule that could no longer change it may not be run at all. Such a rule reports its weight and `outcome: SKIPPED`, and the recipe does **not** set `partial_evidence` — `determined_by` already accounts for the verdict.

## Rules you didn't write

Your recipes can include Prelude-managed rules alongside your own. A managed rule returns its `rule_id`, `weight`, and `outcome`, but not its `name`. It reports `blocked_by: "missing_data"` rather than naming the signal it needed, because its condition isn't disclosed.

The identifier is always returned, so a rule whose condition you can't see is still one you can reweight, switch off, or ask us about.

## The target

The two halves of `target` are also readable from a rule, as `target.phone_number` and `target.email_address` - see [Rules, recipes and flows](/watch/v2/documentation/rules-recipes-flows). The values a rule compares against are the canonical ones: the number in E.164, the address normalized.

The half you omit is **missing evidence, not an empty value**. A rule reading it reports `NOT_EVALUATED` rather than being scored as though the condition were false, exactly as it would for a signal that never arrived. Sending both halves is therefore what makes rules about either of them run.

## Signals

Send the values your rules read in `signals`:

* The [SDK signals the API accepts](#body-signals), such as `ip` or `device_platform`. A rule reads them as `signal.ip`, `signal.device_platform`, and so on. When the front-end SDK also provided a value through `dispatch_id`, the SDK value is used.
* Your own signals, under `ext`: a plan tier, an account's age, a cart total. A rule reads `ext.plan_tier` as `signal.ext.plan_tier`. Values are strings.

```json theme={null}
"signals": {
  "ip": "203.0.113.7",
  "ext": { "plan_tier": "free", "account_age_days": "3" }
}
```

One of your own signals that a rule reads and your request omits counts as **missing evidence, not an empty value**. Rules reading it report `NOT_EVALUATED` rather than being scored as though the condition were false. Sending it with an empty value is the same as omitting it. A key no rule in the flow reads is ignored rather than rejected, so one payload can serve flows that read different keys.

To have a rule read your own value for an SDK signal, for example the IP address your backend saw, send it under `ext` too (`ext.ip`) and read it as `signal.ext.ip`.


## OpenAPI

````yaml post /v2/watch/eval
openapi: 3.1.0
info:
  title: Prelude API
  version: 2.0.0
  description: The Prelude API allows you to send messages to your users.
  contact:
    email: support@prelude.so
servers:
  - url: https://api.prelude.dev
    description: Production server
security:
  - apiToken: []
tags:
  - name: Notify
    description: Send transactional and marketing messages with compliance enforcement.
  - name: Transactional
    description: Send transactional messages (deprecated - use Notify API instead).
  - name: Verify
    description: Verify phone numbers.
  - name: Watch
    description: Evaluate email addresses and phone numbers for trustworthiness.
  - name: Lookup
    description: >-
      Retrieve detailed information about a phone number including carrier data,
      line type, and portability status.
paths:
  /v2/watch/eval:
    post:
      tags:
        - Watch
      summary: Evaluate a flow
      description: >-
        **Beta.** The request and response shapes may still change. Talk to us
        before you build against it. Flows, recipes and rules are authored
        through the Watch Management API, or configured by Prelude on your
        behalf.


        Score a target against the rules configured for one moment in your
        product — signup, checkout, password reset. The flow selects which
        recipes run; each recipe scores its rules against a threshold and
        returns its own verdict, and the evaluation answers with the most severe
        verdict across them. The verdict is advisory: what to do about it is
        yours. Where Predict returns a single model-derived outcome, Eval
        returns the full breakdown, so you can see which rules fired and which
        could not run.
      operationId: evaluateFlow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EvalWatchRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EvalWatchResponse'
        '400':
          description: KO
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                example:
                  code: invalid_request
                  message: >-
                    The provided request is invalid. See `details` field for
                    more information.
                  type: bad_request
                  param: request
                  details:
                    - path: flow_id
                      message: Unknown flow.
                  request_id: 3d19215e-2991-4a05-a41a-527314e6ff6a
components:
  schemas:
    EvalWatchRequest:
      type: object
      properties:
        flow_id:
          type: string
          examples:
            - flo_01jc0t6fwwfgfsq1md24mhyztj
          description: >-
            The flow to evaluate. A flow names the moment you are guarding and
            selects the recipes that run.
        target:
          $ref: '#/components/schemas/WatchEvalTarget'
        signals:
          $ref: '#/components/schemas/WatchEvalSignals'
        dispatch_id:
          type: string
          description: >-
            The identifier of the dispatch that came from the front-end SDK. Its
            signals are used first; an SDK signal in `signals` is used only when
            the dispatch did not provide it.
          examples:
            - 123e4567-e89b-12d3-a456-426614174000
          minLength: 36
          maxLength: 36
      required:
        - flow_id
        - target
    EvalWatchResponse:
      type: object
      properties:
        id:
          type: string
          examples:
            - evl_01jc0t6fwwfgfsq1md24mhyztj
          description: The evaluation identifier.
        verdict:
          type: string
          enum:
            - PASS
            - FLAG
          description: >
            The evaluation-level verdict, being the most severe verdict across
            the recipes that ran.
             * `PASS` - No recipe flagged.
             * `FLAG` - At least one recipe flagged.
        recipes:
          type: array
          items:
            $ref: '#/components/schemas/WatchRecipeResult'
          description: >-
            One result per recipe that ran. A recipe the flow names but that is
            not in service is absent rather than reported as having passed.
      required:
        - id
        - verdict
        - recipes
    Error:
      type: object
      properties:
        code:
          type: string
          description: The error code.
          examples:
            - invalid_phone_number
        message:
          type: string
          examples:
            - >-
              The provided phone number is invalid. Provide a valid E.164 phone
              number.
          description: A human-readable message describing the error.
        type:
          type: string
          examples:
            - bad_request
          description: The error type.
        request_id:
          type: string
          examples:
            - 3d19215e-2991-4a05-a41a-527314e6ff6a
          description: >-
            A string that identifies this specific request. Report it back to us
            to help us diagnose your issues.
        param:
          type: string
          examples:
            - phone_number
          description: The parameter the error refers to, when it applies to a single one.
      required:
        - code
        - message
        - type
    WatchEvalTarget:
      type: object
      description: >
        The end user being evaluated. Send a phone number, an email address, or
        both — at least one is required, and sending both is what to do when you
        hold both.


        Each value is also readable from a rule, as `target.phone_number` and
        `target.email_address`. The half this request omits is missing evidence:
        rules reading it report `NOT_EVALUATED` rather than being scored against
        an empty string.
      properties:
        phone_number:
          type: string
          examples:
            - '+30123456789'
          description: The end user's phone number, in E.164 format.
        email_address:
          type: string
          examples:
            - mail@example.com
          description: The end user's email address.
    WatchEvalSignals:
      allOf:
        - $ref: '#/components/schemas/Signals'
        - type: object
          properties:
            ext:
              type: object
              additionalProperties:
                type: string
              description: >
                Your own signals, which a rule reads as `signal.ext.<key>`.
                Values are strings.


                One that a rule reads and this request omits is treated as
                missing evidence, not as an empty value: the rules reading it
                report `NOT_EVALUATED` rather than being scored as though the
                condition were false. Sending it with an empty value is the same
                as omitting it. A key no rule in the flow reads is ignored
                rather than rejected, so one payload can serve flows that read
                different keys.
              examples:
                - plan_tier: free
                  account_age_days: '3'
      description: >-
        The values the flow's rules read. A rule reads an SDK signal as
        `signal.<name>`; when the front-end SDK also provided it through
        `dispatch_id`, the SDK value is used.
    WatchRecipeResult:
      type: object
      properties:
        recipe_id:
          type: string
          examples:
            - rcp_01jc0t6fwwfgfsq1md24mhyztj
          description: The recipe that produced this result.
        threshold:
          type: integer
          format: int64
          examples:
            - 30
          description: The score at or above which this recipe flags.
        score:
          type: integer
          format: int64
          examples:
            - 20
          description: >-
            The sum of the weights of the rules that triggered, clamped to the
            range 0 to 100. Two scores at a bound are not comparable.
        verdict:
          type: string
          enum:
            - PASS
            - FLAG
          description: >-
            This recipe's own verdict. Normally the score against the threshold,
            unless an overriding rule fired — see `determined_by`.
        partial_evidence:
          type: boolean
          description: >-
            At least one rule could not be evaluated, so the score rests on less
            than the whole recipe. The score is still returned — a partial
            verdict is more useful than none — but it is labeled rather than
            passed off as whole.
        determined_by:
          type: string
          examples:
            - rul_01jc0t6fwwfgfsq1md24mhyztj
          description: >-
            The overriding rule that set `verdict`, present only when a rule
            rather than the score decided it. Without it a recipe can report a
            score under its threshold and still flag, with nothing in the
            payload accounting for the difference.
        rules:
          type: array
          items:
            $ref: '#/components/schemas/WatchRuleResult'
          description: >-
            One result per rule in the recipe, in membership order. Every rule
            runs — a score is only meaningful when complete, so there is no
            short-circuit on the first trigger. The exception is a recipe whose
            verdict an overriding rule has already determined, where a rule that
            could no longer change it may report `SKIPPED` instead.
      required:
        - recipe_id
        - threshold
        - score
        - verdict
        - partial_evidence
        - rules
    Signals:
      type: object
      description: >-
        The signals used for anti-fraud. For more details, refer to
        [Signals](/verify/v2/documentation/prevent-fraud#signals).
      properties:
        ip:
          type: string
          format: ipv4
          description: >-
            The public IP v4 or v6 address of the end-user's device. You should
            collect this from your backend. If your backend is behind a proxy,
            use the `X-Forwarded-For`, `Forwarded`, `True-Client-IP`,
            `CF-Connecting-IP` or an equivalent header to get the actual public
            IP of the end-user's device.
          examples:
            - 203.0.113.123
        device_id:
          type: string
          description: >-
            A unique ID for the user's device. You should ensure that each user
            device has a unique `device_id` value. Ideally, for Android, this
            corresponds to the `ANDROID_ID` and for iOS, this corresponds to the
            `identifierForVendor`.
          examples:
            - 8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2
        device_platform:
          type: string
          enum:
            - android
            - ios
            - ipados
            - tvos
            - web
          description: The type of the user's device.
          examples:
            - ios
        device_model:
          type: string
          description: The model of the user's device.
          examples:
            - iPhone17,2
        os_version:
          type: string
          description: The version of the user's device operating system.
          examples:
            - 18.0.1
        app_version:
          type: string
          description: The version of your application.
          examples:
            - 1.2.34
        user_agent:
          type: string
          description: >-
            The user agent of the user's device. If the individual fields
            (os_version, device_platform, device_model) are provided, we will
            prioritize those values instead of parsing them from the user agent
            string.
          examples:
            - >-
              Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X)
              AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3
              Mobile/15E148 Safari/604.1
        ja4_fingerprint:
          type: string
          description: >-
            The JA4 fingerprint observed for the end-user's connection. Prelude
            will infer it automatically when you use our Frontend SDKs (which
            use Prelude's edge network), but you can also forward the value if
            you terminate TLS yourself.
          examples:
            - t13d1516h2_8daaf6152771_e5627efa2ab1
        is_trusted_user:
          type: boolean
          description: >-
            This signal should indicate a higher level of trust, explicitly
            stating that the user is genuine. Contact us to discuss your use
            case. For more details, refer to
            [Signals](/verify/v2/documentation/prevent-fraud#signals).
          examples:
            - false
        existing_user:
          type: boolean
          description: >-
            Whether the end-user already exists in your system, for example an
            existing account signing in again rather than a first-time signup.
            Unlike `is_trusted_user`, this signal does not bypass fraud checks;
            it is taken into account as one additional anti-fraud signal. For
            more details, refer to
            [Signals](/verify/v2/documentation/prevent-fraud#signals).
          examples:
            - false
    WatchRuleResult:
      type: object
      properties:
        rule_id:
          type: string
          examples:
            - rul_01jc0t6fwwfgfsq1md24mhyztj
          description: >-
            The rule that produced this result. Present whatever the rule's
            visibility, so a rule you cannot see the condition of is still one
            you can reweight, switch off, or ask us about.
        type:
          type: string
          enum:
            - MANAGED
            - CUSTOM
          description: >
            Who authored the rule, which is what says how much of the rest of
            this result you get.
             * `MANAGED` - Prelude-owned, shared with customers: `name` and `version_id` are omitted, and `blocked_by` reports only `missing_data`.
             * `CUSTOM` - Yours: every field is returned.
        name:
          type: string
          examples:
            - high_value_cart_new_account
          description: >-
            The rule's name, present for a rule you authored and omitted for a
            Prelude-managed one. A managed rule's name describes what it looks
            for, which is as much of the condition as the expression is.
        version_id:
          type: string
          examples:
            - 3sL4kqtJlcpXroDTDmJ.o.Jj7bB5dGH6
          description: >-
            The version of the rule that scored — the one this recipe is pinned
            to, or the version current at evaluation time when it is not pinned.
            Present for a rule you authored, and omitted for a Prelude-managed
            one.
        weight:
          type: integer
          format: int64
          examples:
            - 10
          description: What this rule contributes to the recipe's score when it triggers.
        outcome:
          type: string
          enum:
            - TRIGGERED
            - NOT_TRIGGERED
            - NOT_EVALUATED
            - SKIPPED
          description: |
            What the rule concluded.
             * `TRIGGERED` - The condition held; `weight` was added to the score.
             * `NOT_TRIGGERED` - The condition did not hold.
             * `NOT_EVALUATED` - The rule could not run, because something it reads never arrived. This is not a quieter `NOT_TRIGGERED`: it contributed nothing either way, and it is why `partial_evidence` is set on the recipe.
             * `SKIPPED` - The rule was not run, because another rule had already determined the recipe's verdict — see `determined_by`. Nothing was missing and nothing failed, so `partial_evidence` is not set: `determined_by` is what accounts for the recipe's score resting on fewer rules.
        blocked_by:
          type: string
          examples:
            - missing_data
          description: >
            Why the rule could not run, set only when `outcome` is
            `NOT_EVALUATED`.


            A rule you authored names the signal or attribute it waited on,
            since you wrote the expression that reads it. A Prelude-managed rule
            reports `missing_data` and nothing more: the signal it waited on is
            part of a condition that is not disclosed.
        unavailable:
          type: boolean
          description: >-
            The rule could not run for a reason on our side rather than anything
            about your request. `outcome` is `NOT_EVALUATED` and the failure is
            ours to fix.
      required:
        - rule_id
        - type
        - weight
        - outcome
  securitySchemes:
    apiToken:
      type: http
      scheme: bearer

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.