---
title: "Simulate: which policy would match a message (real or synthetic), with the active policies or a draft"
method: POST
path: "/inbound-policies/simulate"
tags: ["Inbound"]
---

# Simulate: which policy would match a message (real or synthetic), with the active policies or a draft

`POST /inbound-policies/simulate`

## Request

- Base URL: `https://api.oveyon.com/v1`
- URL: `https://api.oveyon.com/v1/inbound-policies/simulate`
- Auth: HTTP bearer

Read-only. `message` is a received message id or a synthetic message. Without `policy`/`policyId` the ACTIVE policies are evaluated (a paused one is not); `policy` tests an unsaved draft; `policyId` tests one saved policy even if paused. On a received message the key needs read:inbound too (it reads the mailbox). Every simulate call — synthetic or not — counts against the per-key inbound read limit (120/min by default): over it, 429 rate_limited with `Retry-After: 60`. That is longer than the 30 s the official SDKs wait by default, so they do NOT retry it: the 429 reaches your code, with the Retry-After — wait and call again, or raise the SDK retry-after cap. The answer includes what the engine SAW. The request body is capped at 256 KB (413). Scope read:inbound-policies.

## Request body

- InboundPolicySimulateRequest
  - `message` union, required: A received message id (uuid), or a synthetic message.
    - string, uuid
    - object
      - `from` string
      - `envelopeFrom` string
      - `to` union
        - string
        - string[]
      - `subject` string
      - `text` string
      - `html` string
      - `attachments` object[]
        - `name` string
        - `type` string
      - `sizeKb` integer
      - `spamScore` integer
      - `agentSafety` object
        - `verdict` string
        - `score` integer
      - `dmarc` string
      - `spf` string
      - `dkim` string
      - `listedLink` boolean
      - `thread` object
        - `new` boolean
        - `replies` integer
      - `spamFilter` object: The spam-filter verdict to test `spam_filter.score` with. It counts only for addresses whose mailbox has the spam filter on — exactly as at arrival.
        - `score` number
      - `receivedAt` string, date-time
  - `policy` InboundPolicyInput
    - `name` string, required: Unique in the account.
    - `action` 'hold' | 'mute' | 'tag' | 'restrict_channels' | 'pass' | 'notify', required: `hold` holds for a human decision (notified like the quarantine); `mute` stores without dispatching or notifying (released the same way); `tag` adds tags and CONTINUES; `notify` pings the account Telegram and CONTINUES; `restrict_channels` delivers only to the listed channels; `pass` stops the policy chain (it does not bypass the mailbox quarantine).
    - `conditions` InboundPolicyCondition[], required: All must match (AND).
      - `field` 'sender' | 'sender.domain' | 'envelope_sender' | 'recipient' | 'subject' | 'body' | 'attachment.name' | 'attachment.type' | 'attachment.count' | 'size_kb' | 'spam_score' | 'spam_filter.score' | 'agent_safety.score' | 'agent_safety.verdict' | 'dmarc' | 'spf' | 'dkim' | 'listed_link' | 'thread.new' | 'thread.replies' | 'hour' | 'weekday', required
      - `op` 'equals' | 'contains' | 'starts_with' | 'ends_with' | 'matches' | 'regex' | 'in' | 'at_least' | 'at_most', required
      - `value` union, required: String, number, boolean or a list, depending on the field and operator (`spam_filter.score` takes a decimal, e.g. 6.5).
        - string
        - number
        - boolean
        - union[]
          - union
            - string
            - number
    - `actionParams` object, nullable: `tag`: { tags: [1..5, lowercase, [a-z0-9_.:-]] }. `restrict_channels`: { channels: [{ assocId } | { type: webhook|telegram|slack|forward }] } — assocId is the mailbox↔channel link from GET /v1/inbound/routes/{id}. `notify`: { note?: string ≤ 200 }. Others: none.
    - `mailboxes` union[], nullable: Which mailboxes the policy judges. null or absent = every mailbox of the account (the default); a list = only these — mailbox ids from GET /v1/inbound/routes (integers, or `{ id }` as the policy reads back). Each recipient of a message is judged only by the policies of ITS mailbox plus the every-mailbox ones, in the single account order: a message to two mailboxes can get a different outcome per copy. A mailbox of another account answers 400 unknown_mailbox.
      - union
        - integer
        - object
          - `id` integer, required
    - `version` integer: PUT only, optional: the version you read. A different current version answers 409 inbound_policy_version_conflict instead of overwriting.
  - `policyId` integer: Evaluate this SAVED policy alone — even paused, which is how you check a policy right after creating it (new policies are born paused). Send `policy` OR `policyId`, not both.

## Response `200`

Result

- InboundPolicySimulateResult
  - `holdEnabled` boolean
  - `recipients` object[]: One item per recipient: each is judged by the policies of ITS mailbox plus the every-mailbox ones. On a synthetic message the addresses are matched to the mailboxes of the account the way arrival does (an address with no mailbox is judged only by every-mailbox policies). `matched` and `result` at the top are the FIRST recipient's.
    - `recipient` string, nullable
    - `mailboxId` integer, nullable
    - `spamFilterScore` number, nullable: The spam-filter score THIS copy was judged with: null when its mailbox does not have the spam filter on (on a received message, as recorded at arrival; on a synthetic one, as the mailbox is configured now — an address with no mailbox keeps the score you sent).
    - `matched` object[]
      - `id` integer, nullable
      - `name` string
      - `action` string
    - `result` object
  - `matched` object[]
    - `id` integer, nullable
    - `name` string
    - `action` string
  - `result` object
    - `action` string, nullable
    - `policy` string, nullable
    - `degradedReason` string, nullable: Same values as the decision feed. On a received message the simulator also predicts mailbox_quarantine, no_notice and no_channel from the mailboxes the message went to.
    - `tags` string[]
    - `notify` string[]
    - `channels` object, nullable
  - `seen` object: What the engine saw — so "did not match" tells whether it was the rule or the data.

## Other responses

- `400`: bad_request with a stable `reason` (the `message` follows your language; the `reason` never changes): name_empty, name_too_long, name_invalid, unknown_action, no_conditions, too_many_conditions, conditions_too_large, invalid_condition, unknown_field, invalid_op, empty_value, value_too_long, invalid_value, too_many_items, too_many_wildcards, params_not_accepted, invalid_params, unknown_param, param_required, param_invalid, unknown_channel, mailboxes_invalid, mailboxes_empty, too_many_mailboxes, unknown_mailbox, invalid_version, invalid_order, message_required, invalid_received_at, invalid_simulate, regex_invalid, regex_unsupported, regex_too_long, regex_too_complex, regex_limit. When the problem is one condition, `field` (and `op`) name it; when it is an action parameter, `param` names it; `max`/`min`/`allowed`/`hint` explain the limit.
- `401`: Missing or invalid API key.
- `403`: insufficient_scope — a received message needs read:inbound too (it reads the mailbox); or any of the common 403s (key_disabled, account_suspended…). 403 is not a limit: do not retry.
- `404`: inbound_policy_message_not_found, or inbound_policy_not_found for policyId
- `409`: inbound_policy_unreadable — the saved policy in `policyId` can no longer be read by the engine (it is skipped at arrival); `reason` names the validation that failed. Edit the policy to fix it.
- `413`: payload_too_large — the request body exceeds 256 KB (the inbound-policy routes accept small bodies only), with `limit: 262144`. Refused before any validation. Do not retry the same body.
- `415`: unsupported_media_type — send the body as `application/json` (form and plain-text bodies are not accepted).
- `429`: rate_limited — the per-key inbound read limit, with `Retry-After: 60` (above the SDKs' default 30 s cap, so it is not retried for you); or the general per-key limit.
- `500`: internal_error — an unexpected failure on our side. The detail stays in our logs, never in the body. Retry reads; retry a write only when it is idempotent or carries an idempotency key.
- `503`: ip_rules_unavailable — the IP ranges bound to your key could not be checked right now (caution, not a denial). Retry after `Retry-After` (30 s).

---

[API](https://skmtc.dev/oveyon/apis/oveyon-api.md) · [All operations](https://skmtc.dev/oveyon/apis/oveyon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oveyon/apis/oveyon-api/revisions/abf64c09d297?raw)
