---
title: "Bulk Mark Reviewed"
method: POST
path: "/api/v1/logs/bulk-mark-reviewed"
tags: ["logs"]
---

# Bulk Mark Reviewed

`POST /api/v1/logs/bulk-mark-reviewed`

Record one reviewer's verdict across many logs.

Upserts one review per ``(log, reviewer)`` and writes the log's
``review_status`` **in the same transaction**, honouring the sole-writer
invariant: those four columns are only coherent together, and a bulk path
that wrote ``review_status`` alone would be the fastest possible way to
produce logs that read as reviewed with no verdict behind them.

There is deliberately no bulk *un*-review. The design shows none, and
removal stays in the detail panel.

## Request body

- BulkMarkReviewedRequest
  - `subject` 'voice' | 'text' | 'email' | 'booking', required
  - `ids` string[]
  - `selection` BulkSelection — Either explicit ids or a filter, never both, never neither. **Fail closed.** An explicitly empty ``ids`` matches NOTHING; it never widens to "everything in the view". That is the existing contract for exports (``TestSelectedIdsFailClosed``) and the stakes are higher for a mutation: a buggy caller that sends ``[]`` must delete nothing, not delete the tenant's entire log history.
    - `ids` string[], nullable
    - `filters` LogListParams — The universal filter set, shared by all four log lists. Subject-native parameters (voice's transfer telemetry, email's ``include_spam``, bookings' stay axis) stay declared on their own routes — see the plan's §3.4. This model covers only what all four mean the same way. Pagination is intentionally defaulted to ``None`` rather than a number: the four routes have different historical page sizes (voice 10, text 20, bookings 50) and silently changing one would re-page every existing caller. Each route passes its own default to :meth:`resolve_pagination`.
      - `limit` integer, nullable
      - `offset` integer, nullable
      - `page` integer, nullable — Deprecated — use `offset`.
      - `per_page` integer, nullable — Deprecated — use `limit`.
      - `start_date` string, date, nullable
      - `end_date` string, date, nullable
      - `date_field` string, nullable
      - `date_from` string, date-time, nullable — Deprecated — use `start_date`.
      - `date_to` string, date-time, nullable — Deprecated — use `end_date`.
      - `use_created_at` boolean, nullable — Deprecated — use `date_field=created_at`.
      - `property_ids` string[], nullable — Filter to logs associated with any of these hotels.
      - `property_ids_exclude` string[], nullable — Exclude logs associated with any of these hotels; logs with no hotel are kept, mirroring the widget aggregate.
      - `include_considered_hotels` boolean, nullable — Whether the hotel filter also matches hotels the conversation was merely CONSIDERED for, as well as the hotel it was attributed to. Applies to both property_ids and property_ids_exclude. Unset keeps the historical any-association behaviour during the rollout.
      - `agent_id` string, uuid, nullable — Filter by the agent that handled the log.
      - `outcome_agent_ref` string, nullable — Filter by outcome owner, as 'ai_voice:<uuid>' / 'ai_text:<uuid>' / 'human:<uuid>'. Use '__null__' for unassigned owners.
      - `outcome_agent_refs` string[], nullable — Filter to logs owned by any of these refs.
      - `outcome_agent_refs_exclude` string[], nullable — Exclude logs owned by any of these refs; unassigned owners are kept, mirroring the widget aggregate.
      - `outcome_agent_kind` 'ai_voice' | 'ai_text' | 'human', nullable — Filter by the outcome owner's kind.
      - `reviewed` boolean, nullable
      - `status` string, nullable
      - `has_booking_intent` boolean, nullable — Filter to logs with a recorded booking inquiry.
      - `has_booking` boolean, nullable
      - `booking_made` boolean, nullable
      - `booking_attributed` boolean, nullable
      - `tag_ids` string[], nullable
      - `country` string, nullable
      - `is_after_hours` boolean, nullable
      - `population_filters` string[], nullable — Named conversation-population filters (business_hours, min_call_25s, real_customer_message, exclude_spam_test, dedupe_24h). Unknown names are a 422. A name that does not apply to this subject is a no-op.
      - `search` string, nullable
      - `standard_filters` string, nullable — JSON array of {field, value} for standard-column filters.
      - `metric_filters` string, nullable — JSON array of [{metric_id, outcome}] for multi-metric AND filtering.
      - `sort_by` string, nullable
      - `sort_order` 'asc' | 'desc', nullable
      - `strict` boolean
      - `voice_agent_id` string, uuid, nullable — Deprecated — use `agent_id`.
      - `metric_id` string, uuid, nullable — Deprecated — use `metric_filters`.
      - `metric_state` 'pass' | 'fail' | 'na' | 'not_run' | 'error', nullable — Deprecated — use `metric_filters`.
      - `booking_intent` boolean, nullable — Deprecated — use `has_booking_intent`.
    - `exclude_ids` string[]
    - `booking_filters` BookingListSelectors — The booking-only query parameters of the ``/api/v1/logs/booking`` list. Bookings are the one subject whose filters are mostly NOT in ``LogListParams``: status code, ingestion origin, attribution tier, the provider source id, the discrepancy flag and the two legacy date axes are all booking vocabulary, and the universal params have no spelling for any of them. That matters the moment a mutation acts over "everything matching the current filter" (PR 44). A filter-scoped bulk action carrying only ``LogListParams`` would resolve a **wider** population than the screen showed — the operator filters to ``origin=agent_tool``, and the tag lands on every booking in the date range. So the selectors travel with the selection, and this model is the shape they travel in. Field names follow the route's query parameters, with **two deliberate exceptions**: the route spells the hotel filter ``property`` / ``property_exclude`` (short, because it is typed into URLs) while this model and ``BookingListFilters`` both spell it ``property_codes`` / ``property_codes_exclude`` — the name that says what it holds, since bookings scope by the provider's property CODE rather than by hotel UUID like the three conversation subjects. The booking page maps the alias; everything downstream reads the explicit name. Everywhere else the two agree, so a URL, a saved view and a bulk payload spell a booking filter the same way.
      - `property_codes` string[], nullable
      - `property_codes_exclude` string[], nullable
      - `originating_channel` OriginatingChannel[], nullable
      - `originating_channel_exclude` OriginatingChannel[], nullable
      - `wh_source_id` string, nullable
      - `channelstream` boolean, nullable
      - `status_code` BookingStatusCode[], nullable
      - `origin` IngestionSource[], nullable
      - `attribution_tier` AttributionTier[], nullable
      - `booked_from` string, date-time, nullable
      - `booked_to` string, date-time, nullable
      - `stay_from` string, date, nullable
      - `stay_to` string, date, nullable
      - `cancelled_from` string, date-time, nullable
      - `cancelled_to` string, date-time, nullable
      - `discrepancy` 'feed_conflicts_human' — Booking-anchored discrepancy filter values (PR 6/PR 7). ``judged_without_reservation`` is log-anchored (a stamped log with NO canonical reservation) and therefore lives on the logs surface — a bookings list cannot show a booking that doesn't exist.
      - `unknown_commission` boolean, nullable
  - `verdict` 'good' | 'par' | 'bad', required — Reviewer's coarse verdict on the whole call (V0.5). The three values cover the spectrum without overclassifying: the agent nailed it, met the bar, or fell short. Annotations carry the signal — the verdict is a coarse summary only. See ``sync_review_to_findings.py`` for the uniform "verdict + 0 annotations → 0 findings" rule (intentional extension beyond the briefing's §9, which only guaranteed it for good/par; V0.5 extends it to bad so the sync function has one consistent rule).
  - `outcome` string, required

## Response `200`

Successful Response

- BulkMarkReviewedResponse
  - `updated_count` integer, required
  - `matched_count` integer
  - `failed_ids` string[]

## Other responses

- `409` — This subject does not support the requested capability
- `422` — Malformed request: a bad agent ref, both or neither of ids/selection, a primary tag outside the tag list, or a filter-scoped population over the action's cap

---

[API](https://skmtc.dev/getanana/apis/cleon-api.md) · [All operations](https://skmtc.dev/getanana/apis/cleon-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/getanana/cleon-api/revisions/c0f54348e251/schema)
