> ## 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, and flows and recipes are configured by Prelude on your behalf for now. Talk to us before you build against it.

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 and action across them. 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. Scoring-only — it does not update counters by itself.

<Note>
  **Eval is in beta.** The request and response shapes may still change, and Prelude configures flows and recipes on your behalf for now: you can't author them yourself yet. Talk to us before you build against it.
</Note>

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.type` to `phone_number` or `email_address` with the corresponding value.

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 and action across all of them.

Eval is **scoring-only** and **advisory**. It doesn't update long-lived abuse counters by itself, and `action` 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`. A preempting 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`.

## 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.

## Attributes

Rules can read values only your application knows: a plan tier, an account's age, a cart total. Recipes declare which ones they use. You supply them in `attributes`, keyed without the `attr.` prefix a rule expression uses.

An attribute a recipe declares 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. A key no recipe in the flow declares is ignored rather than rejected, so one payload can serve flows that read different attributes.


## 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, and flows
        and recipes are configured by Prelude on your behalf for now. Talk to us
        before you build against it.


        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 and action across them. 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. Scoring-only — it does not
        update counters by itself.
      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/Target'
          description: The identifier to score — a phone number or email address.
        signals:
          $ref: '#/components/schemas/Signals'
        dispatch_id:
          type: string
          description: >-
            The identifier of the dispatch that came from the front-end SDK.
            Signals it carries fill in anything the request did not state; the
            request wins where both supply a value.
          examples:
            - 123e4567-e89b-12d3-a456-426614174000
          minLength: 36
          maxLength: 36
        attributes:
          type: object
          additionalProperties:
            type: string
          description: >
            Values for the attributes the flow's recipes declare, keyed without
            the `attr.` namespace a rule uses to reference them.


            An attribute a recipe declares 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. A key no recipe in the flow declares is ignored rather
            than rejected, so one payload can serve flows that read different
            attributes.
          examples:
            - plan_tier: free
              account_age_days: '3'
      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.
        action:
          type: string
          enum:
            - ALLOW
            - BLOCK
            - CHALLENGE
          description: >
            What the evaluation suggests you do, being the most severe action
            across the recipes that ran. Advisory: enforcement is yours.
             * `ALLOW` - Let the request through.
             * `BLOCK` - Refuse the request.
             * `CHALLENGE` - Let the request through behind an additional check.
        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
        - action
        - 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.
      required:
        - code
        - message
        - type
    Target:
      type: object
      description: The operation target. Either a phone number or an email address.
      properties:
        type:
          type: string
          enum:
            - phone_number
            - email_address
          description: The type of the target. Either "phone_number" or "email_address".
        value:
          type: string
          examples:
            - '+30123456789'
            - mail@example.com
          description: An E.164 formatted phone number or an email address.
      required:
        - type
        - value
    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
    WatchRecipeResult:
      type: object
      properties:
        recipe_id:
          type: string
          examples:
            - rcp_01jc0t6fwwfgfsq1md24mhyztj
          description: The recipe that produced this result.
        recipe_version:
          type: integer
          format: int64
          examples:
            - 1
          description: >-
            The version of the recipe that ran. A recipe's rules and threshold
            can change, so a stored verdict is only interpretable alongside
            this.
        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 -100 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 a preempting 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 preempting 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.
      required:
        - recipe_id
        - recipe_version
        - threshold
        - score
        - verdict
        - partial_evidence
        - rules
    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.
        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.
        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
          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.
        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
        - weight
        - outcome
  securitySchemes:
    apiToken:
      type: http
      scheme: bearer

````