---
title: "Replace an inbound policy (optional optimistic concurrency with `version`)"
method: PUT
path: "/inbound-policies/{id}"
tags: ["Inbound"]
---

# Replace an inbound policy (optional optimistic concurrency with \`version\`)

`PUT /inbound-policies/{id}`

## Request

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

## Path parameters

- `id` integer, required

## Request body

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

## Response `200`

Updated

- InboundPolicy
  - `id` integer
  - `name` string
  - `position` integer: Evaluation order (0 = first).
  - `action` string
  - `actionParams` object, nullable
  - `conditions` InboundPolicyCondition[]
    - `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
  - `mailboxes` object[], nullable: null = every mailbox of the account; a list = only these mailboxes. An EMPTY list means the chosen mailboxes were deleted: the policy judges no mailbox until you edit it.
    - `id` integer
    - `address` string: `local@domain`, or `*@domain` for a catch-all.
  - `paused` boolean: New policies are born PAUSED at the end of the order: nothing changes until you unpause.
  - `pausedAt` string, date-time, nullable
  - `version` integer
  - `readable` boolean: false = the stored policy can no longer be interpreted and is SKIPPED at arrival until fixed.
  - `effectiveActionToday` string, nullable: List only. What it does TODAY: `tag` when hold/mute/restrict_channels degrade (platform switch off, or the held cap reached). null when paused.
  - `degradedReason` 'hold_disabled' | 'hold_limit' | 'null', nullable: Why the action TODAY is a tag: the platform switch is off (hold_disabled) or the policy already holds its cap (hold_limit). null while paused or unreadable (`readable: false`) — such a policy is not evaluated.
  - `matches30d` integer
  - `lastMatchedAt` string, date-time, nullable
  - `heldNow` integer: Messages this policy holds right now (cap 200 per policy).
  - `createdBy` string, nullable
  - `updatedBy` string, nullable
  - `createdAt` string, date-time
  - `updatedAt` string, date-time, nullable

## 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 — creating or editing a policy needs write:inbound-policies AND read:inbound (a policy on message fields, once active, reveals which messages matched); or any of the common 403s (key_disabled, account_suspended…).
- `404`: inbound_policy_not_found — the policy does not exist or belongs to another account (identical on purpose).
- `409`: inbound_policy_version_conflict or inbound_policy_name_taken
- `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`: Per-key limit (`rate_limited`, 600 requests/min by default per API key — or per MCP OAuth connection —, from any IP), per-IP limit for requests WITHOUT a valid credential (`rate_limited`, 600/min by default), too many unknown or malformed credentials from your address (`too_many_auth_failures`, 20/min by default, counted per IPv4 address or per IPv6 /48; a missing `Authorization`, an expired OAuth token or a disabled key do not count, and anything without the shape of one of our keys counts without being checked; from an address over that limit a valid key still gets through within 120 credential checks/min, and above them every credential not validated in the last 5 minutes gets this 429 until the minute turns) — all checked before the route, on every operation —, account quota (`quota_exceeded` with `window`, `used`, `limit`), warmup cap (`warmup_cap_reached`) or saturation (`queue_full`). Honor `Retry-After` with exponential backoff and jitter.
- `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)
