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

# Backtest Rules

> Evaluate a hypothetical rule configuration against your recent transactions (most recent first, up to the backtest cap) without creating a rule or touching any transaction. Use it before activating a rule to see how often it would have matched and how many subjects it would have affected.

export const AgentPromptAccordion = ({prompt, title = "AI Agent Integration Prompt"}) => {
  const [copied, setCopied] = React.useState(false);
  const handleCopy = e => {
    e.stopPropagation();
    if (!prompt) return;
    navigator.clipboard.writeText(prompt.trim()).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };
  const agents = ["Claude Code", "Codex", "Cursor", "Devin", "Windsurf", "GitHub Copilot"];
  return <div className="didit-agent-card">
      {}
      <div className="didit-agent-titlebar">
        <div className="didit-agent-dots" aria-hidden="true">
          <span className="didit-agent-dot didit-agent-dot-red"></span>
          <span className="didit-agent-dot didit-agent-dot-yellow"></span>
          <span className="didit-agent-dot didit-agent-dot-green"></span>
        </div>
        <span className="didit-agent-filename">{title}</span>
        <button type="button" className={`didit-agent-copy ${copied ? "didit-agent-copy-copied" : ""}`} onClick={handleCopy} title="Copy prompt to clipboard" aria-label={copied ? "Copied!" : "Copy prompt to clipboard"}>
          {copied ? <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <span>Copied</span>
            </> : <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
                <path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" stroke="currentColor" strokeWidth="1.5" />
              </svg>
              <span>Copy</span>
            </>}
        </button>
      </div>

      {}
      <pre className="didit-agent-body"><code>{prompt.trim()}</code></pre>

      {}
      <div className="didit-agent-footer">
        <span className="didit-agent-footer-label">Paste into</span>
        <div className="didit-agent-chips">
          {agents.map(name => <span key={name} className="didit-agent-chip">{name}</span>)}
        </div>
      </div>
    </div>;
};

<AgentPromptAccordion
  title="Backtest Rules API Prompt"
  prompt={`Backtest a hypothetical rule configuration against my recent transactions.

Endpoint:
POST https://verification.didit.me/v3/transactions/rules/backtest/

Authentication:
Use the x-api-key header with my Didit API key.

Request body (application/json):
{
"conditions": [{"field": "currency", "operator": "eq", "value": "EUR"},
{"field": "amount", "operator": "lt", "value": 10000}],
"aggregation": [{"metric": "count", "operator": "gte", "value": 20, "window": "30d",
               "filters": {"direction": "INBOUND", "transaction_type": "finance", "currency": "EUR", "subject_vendor_data": "__current__"}},
              {"metric": "max", "field": "amount", "operator": "lt", "value": 10000, "window": "30d",
               "filters": {"direction": "INBOUND", "transaction_type": "finance", "currency": "EUR", "subject_vendor_data": "__current__"}}],
"evaluation_mode": "ALL",
"scope": {"transaction_types": ["finance"], "directions": ["INBOUND"]},
"period_days": 90          // 1-365, default 90
}

Response:
{ "evaluated": 4820, "matched": 12, "affected_entities": 4, "period_days": 90 }

Behavior:
- Read-only and free: no rule is created, no transaction is modified, no rule runs are recorded.
- Evaluates the most recent transactions in the period, up to the backtest cap.
- affected_entities counts distinct subjects (by vendor_data) among the matches.
- Use it before switching a rule to ACTIVE; a surprising match count means the thresholds need tuning.
- Copy the candidate rule EXACTLY: same conditions, aggregation, evaluation_mode and scope. Omitted conditions, aggregation and scope add no constraints of their own; dropping scope can include directions or transaction types the saved rule skips. Omitted evaluation_mode defaults to ALL (which can reduce matches for an ANY rule), and omitted period_days defaults to 90 days, not unlimited history.

Sandbox fixtures for this example: at least 20 inbound EUR finance transactions for the same subject within 30 days, each with raw amount below 10000. No normalized amount is required. Sandbox ingestion does not evaluate rules; call backtest explicitly.

Failure modes:
- 400 - same condition/aggregation/scope validation as rule create.`}
/>


## OpenAPI

````yaml POST /v3/transactions/rules/backtest/
openapi: 3.0.0
info:
  version: 3.0.0
  title: Didit Verification API
  description: Identity verification API. Authenticate with x-api-key header.
servers:
  - url: https://verification.didit.me
security: []
tags: []
paths:
  /v3/transactions/rules/backtest/:
    post:
      tags:
        - Transactions
      summary: Backtest rule configuration
      description: >-
        Evaluate a hypothetical rule configuration against your recent
        transactions (most recent first, up to the backtest cap) without
        creating a rule or touching any transaction. Use it before activating a
        rule to see how often it would have matched and how many subjects it
        would have affected.
      operationId: backtestTransactionRules
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransactionRuleBacktestRequest'
            example:
              conditions:
                - field: currency
                  operator: eq
                  value: EUR
                - field: amount
                  operator: lt
                  value: 10000
              aggregation:
                - metric: count
                  operator: gte
                  value: 20
                  window: 30d
                  filters:
                    direction: INBOUND
                    transaction_type: finance
                    subject_vendor_data: __current__
                    currency: EUR
                - metric: max
                  field: amount
                  operator: lt
                  value: 10000
                  window: 30d
                  filters:
                    direction: INBOUND
                    transaction_type: finance
                    subject_vendor_data: __current__
                    currency: EUR
              evaluation_mode: ALL
              scope:
                transaction_types:
                  - finance
                directions:
                  - INBOUND
              period_days: 90
      responses:
        '200':
          description: Backtest statistics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionRuleBacktestResult'
              example:
                evaluated: 4820
                matched: 12
                affected_entities: 4
                period_days: 90
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          label: curl
          source: >-
            curl -X POST
            'https://verification.didit.me/v3/transactions/rules/backtest/' \
              -H 'x-api-key: YOUR_API_KEY' \
              -H 'Content-Type: application/json' \
              -d '{"conditions": [{"field": "currency", "operator": "eq", "value": "EUR"}, {"field": "amount", "operator": "lt", "value": 10000}], "aggregation": [{"metric": "count", "operator": "gte", "value": 20, "window": "30d", "filters": {"direction": "INBOUND", "transaction_type": "finance", "subject_vendor_data": "__current__", "currency": "EUR"}}, {"metric": "max", "field": "amount", "operator": "lt", "value": 10000, "window": "30d", "filters": {"direction": "INBOUND", "transaction_type": "finance", "subject_vendor_data": "__current__", "currency": "EUR"}}], "evaluation_mode": "ALL", "scope": {"transaction_types": ["finance"], "directions": ["INBOUND"]}, "period_days": 90}'
        - lang: python
          label: Python
          source: >-
            import os


            import requests


            resp = requests.post(
                'https://verification.didit.me/v3/transactions/rules/backtest/',
                headers={'x-api-key': os.environ['DIDIT_API_KEY']},
                json={
                "conditions": [
                    {
                        "field": "currency",
                        "operator": "eq",
                        "value": "EUR"
                    },
                    {
                        "field": "amount",
                        "operator": "lt",
                        "value": 10000
                    }
                ],
                "aggregation": [
                    {
                        "metric": "count",
                        "operator": "gte",
                        "value": 20,
                        "window": "30d",
                        "filters": {
                            "direction": "INBOUND",
                            "transaction_type": "finance",
                            "subject_vendor_data": "__current__",
                            "currency": "EUR"
                        }
                    },
                    {
                        "metric": "max",
                        "field": "amount",
                        "operator": "lt",
                        "value": 10000,
                        "window": "30d",
                        "filters": {
                            "direction": "INBOUND",
                            "transaction_type": "finance",
                            "subject_vendor_data": "__current__",
                            "currency": "EUR"
                        }
                    }
                ],
                "evaluation_mode": "ALL",
                "scope": {
                    "transaction_types": [
                        "finance"
                    ],
                    "directions": [
                        "INBOUND"
                    ]
                },
                "period_days": 90
            },
                timeout=30,
            )

            resp.raise_for_status()

            stats = resp.json()

            print(stats['matched'], 'matches across',
            stats['affected_entities'], 'subjects')
components:
  schemas:
    TransactionRuleBacktestRequest:
      type: object
      properties:
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/TransactionRuleCondition'
          default: []
        aggregation:
          type: array
          items:
            $ref: '#/components/schemas/TransactionRuleAggregation'
          default: []
        evaluation_mode:
          type: string
          enum:
            - ALL
            - ANY
          default: ALL
        scope:
          $ref: '#/components/schemas/TransactionRuleScope'
        period_days:
          type: integer
          minimum: 1
          maximum: 365
          default: 90
      description: >-
        Hypothetical rule configuration to evaluate against recent transactions.
        Read-only and free: nothing is created or modified. Send the same
        conditions, aggregation, evaluation_mode AND scope as the rule you
        intend to activate. Omitted conditions, aggregation and scope add no
        constraints of their own; dropping scope can include directions or
        transaction types the saved rule skips. Omitted evaluation_mode defaults
        to ALL (which can reduce matches for an ANY rule), and omitted
        period_days defaults to 90 days, not unlimited history.
    TransactionRuleBacktestResult:
      type: object
      properties:
        evaluated:
          type: integer
          description: Transactions evaluated (most recent first, capped).
        matched:
          type: integer
          description: Transactions the configuration would have matched.
        affected_entities:
          type: integer
          description: Distinct subjects (by vendor_data) among the matches.
        period_days:
          type: integer
    TransactionRuleCondition:
      type: object
      required:
        - field
        - operator
      properties:
        field:
          type: string
          description: >-
            Transaction field path to evaluate. Core fields: amount, currency,
            direction, action_type, score, severity, transaction_type,
            payment_details, preferred_currency_amount (falls back to
            amount_in_default_currency), default_currency_code, subject_country,
            counterparty_country, subject_vendor_data, counterparty_vendor_data,
            subject_device_fingerprint, counterparty_device_fingerprint,
            subject_browser_family, subject_browser_version, subject_platform,
            subject_accept_language, subject_session_age_ms, subject_ip_country,
            counterparty_ip_country, subject_ip_address,
            counterparty_ip_address, subject_payment_method_type,
            subject_payment_method_country, subject_payment_method_fingerprint,
            subject_payment_method_account_id (and the
            counterparty_payment_method_* equivalents), travel_rule_status,
            travel_rule_required, travel_rule_obligations_count,
            subject_days_since_previous_transaction, tags, and
            custom_values.<key> for any key submitted in custom_properties on
            transaction create. Country-code fields accept ISO-2 or ISO-3; both
            sides are normalized to ISO-3 before comparison.
        operator:
          type: string
          enum:
            - eq
            - ne
            - gt
            - gte
            - lt
            - lte
            - in
            - not_in
            - contains
            - not_contains
            - contains_any
            - regex
            - fuzzy_match
            - exists
            - is_not_empty
            - is_not_null
            - not_exists
            - is_empty
            - is_null
          description: >-
            Comparison operator. in/not_in test membership (value may be a list
            or a single scalar). contains/not_contains are case-insensitive
            substring checks against a string value. contains_any takes a list
            of strings and matches when any is a case-insensitive substring of
            the field value. regex matches the value pattern against the field.
            fuzzy_match compares against the value string with the required
            `score` threshold (0-100). gt/gte/lt/lte never match when either
            side is missing. exists/is_not_empty/is_not_null (aliases) and
            not_exists/is_empty/is_null (aliases) are presence checks that
            ignore `value`.
        value:
          description: >-
            Comparison value. Its shape depends on the operator and value_type;
            omit for the presence-check operators.
        value_type:
          type: string
          enum:
            - list
            - field
            - relative_date
          description: >-
            Optional value interpretation. `list`: value is a List UUID and the
            list's entries become the comparison value. `field`: value is
            another field path, compared field-to-field. `relative_date`: value
            is a relative-date object, e.g. {"direction": "past", "unit":
            "days", "amount": 30}.
        score:
          type: number
          minimum: 0
          maximum: 100
          description: >-
            Required when operator is fuzzy_match: the 0-100 similarity
            threshold.
        group_index:
          type: integer
          description: >-
            Optional grouped logic. When any condition carries group_index,
            conditions sharing an index form a group and grouped evaluation
            replaces evaluation_mode.
        group_logic:
          type: string
          enum:
            - AND
            - OR
          default: AND
          description: How conditions combine WITHIN a group.
        groups_logic:
          type: string
          enum:
            - AND
            - OR
          default: OR
          description: >-
            How group outcomes combine ACROSS groups. Read from the first
            condition.
      description: >-
        A condition reads only the transaction being evaluated - it never
        filters the aggregation window.
    TransactionRuleAggregation:
      type: object
      required:
        - value
      properties:
        metric:
          type: string
          enum:
            - count
            - sum
            - max
            - min
            - avg
            - distinct_count
          default: count
          description: >-
            How the matching historical transactions are aggregated.
            unique_count is accepted as an alias of distinct_count.
        field:
          type: string
          default: amount
          description: >-
            Field aggregated by sum/max/min/avg/distinct_count. Ignored for
            count.
        operator:
          type: string
          enum:
            - eq
            - ne
            - gt
            - gte
            - lt
            - lte
          default: eq
          description: Comparison applied to the computed metric.
        value:
          type: number
          description: Threshold the computed metric is compared against.
        window:
          type: string
          pattern: ^\d+(m|h|d)$
          default: 1d
          description: >-
            Look-back window ending at the transaction's txn_date: <N>m
            (MINUTES, not months), <N>h (hours) or <N>d (days), e.g. 30m, 24h,
            7d, 30d.
        filters:
          type: object
          additionalProperties: true
          description: >-
            Narrows which historical transactions are aggregated. Keys are field
            paths from the condition field catalog; a value of "__current__"
            resolves to the same field's value on the transaction being
            evaluated (e.g. {"subject_vendor_data": "__current__"} scopes the
            window to the same subject), and a list value means membership.
            Filters compare by equality or membership ONLY - there is no lt/gte
            filter, so a range such as "historical transactions under 10,000"
            cannot be expressed here. Put the range on the metric instead
            (metric max with operator lt keeps every transaction in the window
            below the threshold).
      description: >-
        Velocity check over the application's historical transactions inside the
        window. Every aggregation entry must match for the rule to match,
        regardless of evaluation_mode. An aggregation constrains only the
        window; the transaction being evaluated is constrained by scope and
        conditions, so repeat direction, currency or transaction_type on both
        halves when the rule promises them.
    TransactionRuleScope:
      type: object
      properties:
        transaction_types:
          type: array
          items:
            type: string
          description: >-
            Restrict to transaction types, e.g. finance, kyc, travel_rule,
            user_event. Empty or missing = no restriction.
        directions:
          type: array
          items:
            type: string
            enum:
              - INBOUND
              - OUTBOUND
          description: Restrict to transaction directions (case-insensitive on write).
        action_types:
          type: array
          items:
            type: string
          description: Restrict to your action_type values, e.g. withdrawal, deposit.
      description: >-
        Which transactions the rule applies to. Only these three keys are
        accepted.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Your application's API key, from Developers -> API keys in the Business
        Console. The primary key has full access. A named key can be scoped:
        none, read or write per resource, limited to some workflows or to
        approved sessions, to a list of IP addresses, and to an expiry date. 401
        means the key is missing, wrong, revoked or expired; 403 means the key
        has no access to this resource or action, or the request came from an
        address outside its IP list; 404 on a session route means the session is
        outside the key's workflows or statuses. A key without media access
        receives image, video and PDF URLs as null, and a key without sessions
        write receives session links and tokens as null. See
        https://docs.didit.me/console/api-keys.

````

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