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

# Identity-rule simulation: estimate the blast radius before you save

> Run the what-if simulation endpoint before enabling or changing identity match rules or the survivorship policy — see how many clusters would form, how many profiles would merge, whether any rule over-merges, and which field value would win per merge pair.

# Identity-rule simulation

Saving a match rule or survivorship policy takes effect immediately. `PUT /cdp/identity-rules` and `PUT /cdp/survivorship-policy` go live the moment the save returns; if a proposed rule over-links (the classic "everyone share `email=info@example.com`" failure), thousands of contacts can merge before anyone notices, and unwinding a merge is hard. The simulation endpoint is the missing what-if step: it runs the same deterministic engine against a bounded sample of your live contacts and reports what your proposed rule set *would* do — without writing anything.

This guide covers the simulation endpoint end to end: why you run it before saving, what you send it, and how to read the blast-radius report it returns.

## 1. Why simulate first

The recommended workflow is to treat simulation as a gate on every identity-rule change:

1. Run the simulation with your proposed rules.
2. Read the blast-radius report.
3. Only when the estimate looks right, PUT the rules (and the survivorship policy, if you are changing it).

The dashboard enforces this gate for the three strong identifier types that the resolver actually matches on — `external_id`, `email`, and `phone`. When you open an edit on one of those shapes, the identity console refuses to save until you have run a simulation for that shape in the current browser session; attempting to save without one returns a toast naming the simulation to run. The gate is session-scoped: a re-simulation in this same session covers all three of those types until you leave the page, and you re-open it to re-run whenever the estimate was wrong.

Three things never trigger the gate, by design: inserting a priority-only re-order (no new merge risk), deleting a rule (the resolver falls back to the seeded default for that shape), and the `anonymous_id` shape, which is platform-owned and carries no attribution risk.

## 2. The endpoint

```
POST https://api.orbit.devotel.io/api/v1/cdp/identity-resolution/simulate
```

Send the proposed match rules on the strong identifier types, plus an optional proposed survivorship policy and an optional probabilistic threshold:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/identity-resolution/simulate \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "match_rules": [
      { "match_type": "external_id", "enabled": true,  "priority": 100 },
      { "match_type": "email",       "enabled": true,  "priority": 200 },
      { "match_type": "phone",       "enabled": false, "priority": 300 }
    ],
    "survivorship_policy": {
      "rules": [
        { "field": "email", "strategy": "most_recently_updated" },
        { "field": "phone", "strategy": "prefer_non_null" }
      ],
      "default_strategy": "prefer_non_null"
    },
    "auto_merge_threshold": 0.8,
    "over_merge_cluster_size": 25,
    "sample_size": 20,
    "max_contacts": 5000
  }'
```

**`match_rules`** (required) is the array being simulated. Each entry sets `match_type` to one of the strong shapes the deterministic resolver matches on — `external_id`, `email`, or `phone` — plus an `enabled` flag and a priority between 1 and 1000. These are the shapes a simulation can toggle; the anonymous-ID shape is never a proposed rule here because the engine has no anonymous-ID field on its contact projection. Duplicate `match_type` entries are rejected with 422.

**`survivorship_policy`** (optional) previews which value would win per field on the sample merge pairs, using the same rule shape as the survivorship policy builder: per-field rules with a `strategy` (one of `prefer_target`, `prefer_source`, `most_recently_updated`, `most_recently_created`, `prefer_non_null`, or `prefer_source_system` plus a `source_system` for the last one) and an optional `default_strategy` applied to every field without an explicit rule. If you omit the policy, the simulator resolves the sample pairs against your tenant's currently active policy, so you see what would happen as the merge would do it today rather than against an empty policy.

**`auto_merge_threshold`** (optional, 0–1) sets the probabilistic threshold the simulation uses to decide which cluster counts as an auto-merge versus a review candidate. If omitted, it falls back to the platform default.

**`over_merge_cluster_size`** (optional) sets the cluster-size threshold for the over-merge guard. Any proposed cluster bigger than this many profiles is flagged with the offending rule named. Defaults to 25 profiles; you can raise it for very large tenants or lower it when testing a deliberately conservative rule.

**`sample_size`** (optional) — the floor is 20. You may ask for more, never fewer, because the review-queue send affordance and the acceptance bar both depend on seeing the matched pairs.

**`max_contacts`** (optional) caps how many contacts the simulation scans (default 5,000; hard ceiling 10,000). Like the deterministic preview endpoint, the simulation never pulls a tenant's whole contact table into one process; when the scan hits the ceiling, `truncated` reports `true` so you can raise `max_contacts` (or accept the bound) rather than silently see a partial picture.

## 3. Reading the response

A successful response wraps the blast-radius report in the standard data envelope:

```json theme={null}
{
  "data": {
    "scanned_contacts": 420,
    "truncated": false,
    "auto_merge_threshold": 0.8,
    "over_merge_cluster_size": 25,
    "clusters_formed": 3,
    "profiles_merged": 2,
    "max_cluster_size": 5,
    "delta_vs_active": {
      "clusters_formed": 1,
      "profiles_merged": 1,
      "max_cluster_size": 0
    },
    "sample_pairs": [
      {
        "primary_id": "cnt_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
        "secondary_id": "cnt_01J8Z9K3P4Q5R6S7T8U9V0W1X9",
        "confidence": 0.92,
        "band": "auto_merge",
        "matched_key": {
          "match_type": "email",
          "value_masked": "s•••@example.com"
        }
      }
    ],
    "sample_pairs_available": 14,
    "survivorship_preview": [
      {
        "primary_id": "cnt_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
        "secondary_id": "cnt_01J8Z9K3P4Q5R6S7T8U9V0W1X9",
        "fields": [
          {
            "field": "email",
            "strategy": "most_recently_updated",
            "winning_side": "secondary",
            "winning_value": "sara@example.com",
            "losing_value": "sara.old@example.com"
          }
        ]
      }
    ],
    "over_merge_flags": [
      {
        "primary_id": "cnt_01H7ABC",
        "cluster_size": 87,
        "match_type": "email",
        "matched_value_masked": "i•••@example.com",
        "rule_priority": 200
      }
    ],
    "over_merge_detected": true
  }
}
```

**Headline aggregates.** `clusters_formed` counts the clusters the proposed rules would stitch; `profiles_merged` counts secondary members of clusters whose confidence clears the auto-merge threshold (below-threshold clusters stay as proposed review candidates and do not inflate the blast-radius count); `max_cluster_size` reports the largest cluster's member count. `delta_vs_active` diffs those three numbers against your currently active rules, so `clusters_formed: 1` reads "one more cluster than today" if positive and "one fewer" if negative.

**Sample merge pairs.** `sample_pairs` lists the concrete (primary, secondary) pairs ranked by confidence, each carrying the `matched_key` that stitched that pair — the match type plus the masked value — and a `band` of `auto_merge` or `review` for how the dashboard would treat the fold-in. `sample_pairs_available` reports the total count that could have been sampled: when that count is below 20 on a small tenant, the endpoint returns every pair that exists and sets `sample_pairs_available` to the actual total so the floor is explicitly satisfied, never silently capped.

**Survivorship preview.** For the first few sample pairs (up to 10), the simulator resolves the proposed (or, if you omitted it, the currently active) survivorship policy against the pairs' actual field values and reports, per governed field: the `field`, the `strategy` applied (explicit rule first, then `default_strategy`, otherwise the `prefer_non_null` fallback), the `winning_side` (`"primary"` the surviving record or `"secondary"` the one folded away), and both the `winning_value` and `losing_value`. This is the exact same strategy the live merge path applies.

**Over-merge guard.** When any proposed cluster exceeds `over_merge_cluster_size`, `over_merge_flags` names the offending rule directly: the flagged cluster's `primary_id`, its `cluster_size`, the `match_type` and `matched_value_masked` that bind the most members in it (the classic "everyone shares `email=info@…`" failure names the shared value), and that rule's `rule_priority` when you supplied one, so the identity console can jump straight to the offending row. `over_merge_detected` flips to `true` when at least one flag fired; treat a flagged proposal as not save-ready — narrow the rule set (disable the aggressive shape or raise its priority so a stronger identifier resolves first) and re-run before you save.

A `422` response means the request failed validation — a duplicate `match_type`, an out-of-range `auto_merge_threshold`, or a missing `source_system` on a `prefer_source_system` rule — and comes back with the standard `issues` list naming the field and the problem.

## 4. The read-only guarantee

The simulation never writes. Its handler issues SELECT statements only — a bounded scan of candidate contacts plus a second bounded fetch of full field rows for the first few preview pairs — and never touches the merge path. There is no merge repository or merge call reachable from the simulation controller; the product's continuous test suite asserts that no write operation can appear there. `delta_vs_active` exists because the simulation re-runs the current configuration on the same contact sample you are proposing against, so the diff is apples-to-apples.

## 5. Authorization and rate limits

Simulating touches PII-bearing contact rows even though it never merges, so the endpoint holds to the same posture as the sibling preview and merge-candidates reads: owner, admin, or developer role plus the `contacts:read` scope. The endpoint is rate-limited to 20 calls per minute per tenant — heavier than the plain preview's 30/min because it also resolves survivorship on a handful of pairs.

## See also

* [Identity rules and the survivorship builder on the CDP pages](/guides/cdp-data-catalog-and-identity-rules) — the authoring side: match shapes, priority, and the survivorship policy
* [Identity resolution and merge semantics](/concepts/cdp-identity-resolution) — the concept: how identifiers resolve, fold, and propagate
* [Identity resolution](/guides/identity-resolution) — the merge review queue and stitching timeline on Audience
* [Stitch anonymous sessions into known contacts](/guides/anonymous-identity-stitching) — why the anonymous-ID shape is never a simulation rule
