---
title: "Correct Classification"
method: PATCH
path: "/calls/{call_id}/correct-classification"
tags: ["calls"]
---

# Correct Classification

`PATCH /calls/{call_id}/correct-classification`

## Request

- Base URL: `https://dev.whispa.net` (the document declares no server; this is the origin it was published from)
- URL: `https://dev.whispa.net/calls/{call_id}/correct-classification`
- Auth: one of:
  - OAuth 2
    - Password
    - Token URL: `auth/bearer/login`
  - API key in cookie `access_token`

Correct a finalized call's answered_by / conversation_outcome (Admin only).

Supervised review of autonomous campaigns: the classifier's label drives the
retry ladder and the CRM note, so a reviewer needs to overrule it — and the
correction reverses what the wrong label caused, reopening a campaign
membership it had closed. Admin-only and audited (every change writes a
``call_classification_corrections`` row), because it edits the record a
customer's CRM has already been told about.

Args:
    call_id: ID of the call to correct
    payload: New answered_by and/or conversation_outcome, plus an optional
        reason recorded on the audit row

Returns:
    The corrected call, and what the correction changed beyond the two
    fields (membership reopened, artifacts invalidated)

## Path parameters

- `call_id` string, uuid, required

## Request body

- CorrectClassificationRequest: Admin correction of a finalized call's classification. Both fields are optional; at least one must be present. Omitting a field leaves it alone, which is why ``conversation_outcome`` distinguishes "absent" from "explicitly null" via ``model_fields_set`` — sending null is a request to clear it.
  - `answered_by` 'human' | 'voicemail' | 'unclear': Who answered the call (AI-classified from transcript). This is the first stage of call classification - determining whether a human or voicemail answered the call.
  - `conversation_outcome` 'engaged' | 'refused' | 'wrong_person' | 'other': AI-classified judgement of what kind of conversation occurred. Only applicable when answered_by=HUMAN. Runtime actions such as scheduling a callback or transferring the caller are recorded independently in ``WorkflowDisposition`` and must never overwrite this judgement.
  - `note` string, nullable: Why the correction was made; stored on the audit row

## Response `200`

Successful Response

- CorrectClassificationResponse: The corrected call plus what the correction changed beyond the two fields. The side effects are the point of the feature — a reviewer needs to know whether the campaign actually reopened and what a re-marked non-human call just threw away — so they are reported explicitly instead of being left for the client to infer from the refreshed detail.
  - `call` CallDetailResponse, required: Full call details including transcript and analyses.
    - `id` string, uuid, required
    - `user_id` string, uuid, nullable, required
    - `team_id` string, uuid, nullable: Team snapshot for this call
    - `user_email` string, nullable: Email of the agent who took the call
    - `user_display_name` string, nullable: Display name of the agent who took the call
    - `call_mode` 'training' | 'production' | 'ai_agent': Call mode enumeration.
    - `call_source` 'live' | 'uploaded': Call source enumeration.
    - `scenario` string, nullable, required
    - `agent_id` string, nullable, required
    - `extension` string, nullable: Phone extension that handled the call
    - `called_number` string, nullable: Inbound destination number reported by the provider, when available
    - `telephony_call_id` string, nullable: External telephony system call ID
    - `provider_call_ref` object, nullable: Provider-native call reference (e.g. TCN call_sid/call_type, AWS Connect contact_id) when the provider hosts the call recording. Only some providers support minting a link via /calls/{call_id}/provider-recording-url (TCN today; the endpoint returns 404 for others). For awsconnect this is the stored reference plus instance_alias resolved from the current Connect integration config, which together address the Connect console's contact page — so the alias is whatever is configured now, not whatever was configured when the call happened
    - `suspensions` object[]: Park windows during the call (TCN payment-line transfers): [{suspended_at, resumed_at}] ISO timestamps; resumed_at is null when the call never resumed (grace expired)
    - `timeline` object[]: Typed call lifecycle events in order: {type, at, detail?, source}. Types: ringing, connected, hold_start, hold_end, consult_start, consult_end, call_suspended, call_resumed, call_ended. Rendered as transcript markers and fed to the summary/coaching/QA prompts
    - `customer_number` string, nullable: Phone number of the caller/customer
    - `customer_name` string, nullable: Name of the caller/customer if available
    - `ring_duration_seconds` number, nullable: Time spent ringing before call was answered (seconds)
    - `call_direction` 'incoming' | 'outgoing' | 'internal': Direction of the call. - INCOMING: Customer called agent - OUTGOING: Agent called customer - INTERNAL: Extension-to-extension call
    - `audio_chunks_received` integer, nullable: Number of audio chunks received (0 = no audio captured)
    - `audio_file_path` string, nullable: S3 path to the call recording, when one is archived
    - `started_at` string, date-time, required
    - `ended_at` string, date-time, nullable, required
    - `duration_seconds` integer, nullable, required
    - `is_starred` boolean: Whether this call has been saved for later review
    - `transcript` object[]: Transcript entries (populated from call_transcript_chunks table)
    - `tool_events` object[]: Terminal tool-call events from the autonomous voice agent (persisted tool_call session events), in call order. Payload shape matches the SSE tool_call channel; interleaved into the transcript view behind a toggle.
    - `summary_markdown` string, nullable, required
    - `summary` object, nullable: Structured call summary (new format)
    - `coaching` object, nullable, required
    - `qa_score` integer, nullable: Overall QA scorecard score (mirror of analyzer_artifacts['qa_scorecard']['overall_score']).
    - `tags` CallTagsPayload: Structured detected tags persisted on a call.
      - `tags` CallTagItem[]: Detected tags for this call
        - `key` string, required: Machine-safe tag identifier
        - `evidence` string[]: Short transcript snippets supporting the detected tag
    - `call_outcome` CallOutcome: Canonical call-outcome record, adding service-derived total fields.
      - `commitment_shape` 'new_arrangement' | 'immediate_payment' | 'settlement' | 'arrangement_repair' | 'promise_reset' | 'deferral' | 'refused' | 'other', required: Taxonomy of commitment outcomes extracted from a collections call.
      - `commitment_horizon` 'immediate' | 'within_7d' | 'within_30d' | 'beyond_30d' | 'indefinite': When the committed money is expected to land.
      - `is_new_commitment` boolean, required: True when this call produced new money intent. False for arrangement_repair and all non-monetary shapes.
      - `installment_amount` number, nullable: Per-payment amount in major currency units (e.g. 40.00 for $40). For one-off commitments, equals the one-off amount. Null when no amount is stated.
      - `first_payment_amount` number, nullable: Down payment / first payment amount taken on the call, SEPARATE from the rest of the money agreed on the same call. Example: '$1000 today, then $100 every fortnight' → first_payment_amount=1000, installment_amount=100. Also covers a split lump sum ('half now, half next week' → first_payment = today's half, installment = the remaining one_off payment). Null when the call has no immediate payment component. For a standalone immediate payment (with nothing to follow), use shape=immediate_payment and installment_amount — do NOT use this field.
      - `payment_frequency` 'one_off' | 'weekly' | 'fortnightly' | 'monthly': Cadence of a scheduled commitment.
      - `installment_count` integer, nullable: Exact number of installment_amount payments (NOT counting any first_payment_amount), ONLY when explicitly stated in the transcript. Null for vague durations ('a few months', 'ongoing').
      - `explicit_total_amount` number, nullable: Total commitment value in major currency units, ONLY when the transcript states the total directly. DO NOT compute totals from installment × count — the backend handles that derivation.
      - `confidence` number, required: Classifier's self-reported confidence in this extraction
      - `evidence_quote` string, required: Short transcript excerpt that supports the classification
      - `total_commitment_amount` number, nullable: Total commitment in major currency units. Either copied from `explicit_total_amount` when present, or derived as `installment_amount × installment_count` when both are known. Null when neither is available (open-ended plan, truncated transcript, or non-monetary shape).
      - `total_commitment_source` 'explicit' | 'derived' | 'not_available': Provenance of `total_commitment_amount`.
    - `client` string, nullable: End-client the call collected for, classified against the scenario's configured client list ('Other' for an explicitly named unconfigured creditor; 'Unclear' for insufficient or ambiguous context; null when no classification result was produced). Full result with disposition, confidence, and evidence in analyzer_artifacts['client_classification'].
    - `client_id` string, uuid, nullable: Stable client identity for the display-name snapshot
    - `client_summary` ClientSummary: Read-safe client context embedded in call detail responses.
      - `id` string, uuid, required
      - `name` string, required
      - `description` string, nullable
      - `has_guidelines` boolean
    - `crm_link` CrmCallLinkRead
      - `id` string, uuid, required
      - `provider` string, required
      - `decision` 'linked' | 'none_of_these', required
      - `account_id` string, nullable
      - `account_row_id` string, uuid, nullable
      - `account_display_name` string, nullable
      - `account_number` string, nullable
      - `customer_id` string, nullable
      - `client_id` string, nullable
      - `matched_phone` string, nullable
      - `matched_on` string, nullable
      - `linked_by_user_id` string, uuid, nullable
      - `linked_at` string, date-time, required
      - `identity` CrmIdentityReferenceRead: Minimal linked-account identity facts needed by IDV cards.
        - `full_name` string, nullable
        - `phones` string[]
        - `address` string, nullable
        - `dob` string, date, nullable
        - `email` string, nullable
        - `ssn_last4` string, nullable
        - `postal_code` string, nullable
        - `account_number` string, nullable
        - `customer_id` string, nullable
      - `context` AccountContextRead: Wire shape for domain-shared account context (debt-collection state). Mirrors `src.crm.base.AccountContext`. Every field is optional because providers expose different subsets, and within a single provider some fields are only present when the underlying record has them set.
        - `product_name` string, nullable
        - `account_status` string, nullable
        - `account_status_disposition` 'workable' | 'not_workable' | 'resolved' | 'unknown': Provider-neutral meaning of a CRM's native account status. Providers keep their raw status in ``AccountContext.account_status`` for display and audit, then map it onto this small contract for autonomous outbound policy. Adding another CRM means adding its native-code mapping in that provider; the compliance gate never needs vendor status strings.
        - `collection_stage` 'open' | 'arrangement' | 'broken_arrangement' | 'paid' | 'settled' | 'excluded' | 'unknown': Provider-neutral collections-funnel position of a CRM's native status. Orthogonal to :class:`AccountStatusDisposition`, which answers "may the autonomous scheduler dial this account". The stage answers "where does this debt sit in the collections funnel" for reporting: an arrangement in place and a broken arrangement are both not-workable, but they are opposite outcomes on a performance dashboard. ``EXCLUDED`` marks accounts whose balance can move without money being collected (disputed, deceased, bankrupt, statute-barred), so recovery metrics can leave them out rather than count adjustments as payments.
        - `arrears_amount` number, nullable
        - `arrears_days` integer, nullable
        - `outstanding_amount` number, nullable
        - `original_balance` number, nullable
        - `amount_paid` number, nullable
        - `last_payment_date` string, date, nullable
        - `principal_outstanding` number, nullable
        - `referred_date` string, date, nullable
        - `interest_overdue` number, nullable
        - `interest_rate` number, nullable
        - `late_fees` number, nullable
        - `dishonour_fees` number, nullable
        - `settled_date` string, date, nullable
        - `cycle_day` integer, nullable
        - `repayment_amount` number, nullable
        - `repayment_frequency` string, nullable
        - `next_installment_amount` number, nullable
        - `next_installment_date` string, date, nullable
        - `has_hardship` boolean, nullable
        - `hardship_requested` boolean, nullable
        - `hardship_status` string, nullable
        - `hardship_notice_date` string, date, nullable
        - `active_arrangement_summary` string, nullable
        - `active_arrangement_status` string, nullable
        - `active_arrangement_next_amount` number, nullable
        - `active_arrangement_next_date` string, date, nullable
        - `collection_statuses` string[]
        - `risk_segmentation` string, nullable
        - `num_active_loans` integer, nullable
        - `num_broken_arrangements_last_6_months` integer, nullable
        - `num_dishonours_last_6_months` integer, nullable
        - `num_contacts_last_30_days` integer, nullable
        - `third_party_authority` boolean, nullable
        - `is_staff_account` boolean, nullable
        - `is_outsourced` boolean, nullable
        - `outsource_partner` string, nullable
        - `is_written_off` boolean, nullable
        - `joint_parties` JointPartyRead[]
          - `customer_id` string, nullable
          - `display_name` string, nullable
          - `relationship` string, nullable
        - `recent_transactions` LedgerEntryRead[]
          - `occurred_on` string, date, nullable
          - `description` string, nullable
          - `amount` number, nullable
          - `balance_after` number, nullable
          - `kind` string, nullable
        - `recent_contacts` ContactEventRead[]
          - `occurred_on` string, date, nullable
          - `description` string, nullable
          - `channel` string, nullable
          - `completed` boolean, nullable
          - `done_date` string, date, nullable
        - `recent_successful_contact_dates` string[]: Provider-classified successful contacts for outbound frequency caps; duplicate dates represent separate contacts.
        - `days_past_section_88` integer, nullable
        - `days_past_information_request` integer, nullable
        - `days_past_six_q` integer, nullable
    - `tts_voice_id` string, nullable: Provider-qualified voice the autonomous agent spoke with, stamped at dispatch from the resolved fallback chain — not re-derived from the scenario, which may have been re-voiced since. Null on human calls. Resolve the display name against the voice catalog (GET /ai-calls/voices).
    - `tts_model_id` string, nullable: Provider-qualified TTS model the autonomous agent used, stamped at dispatch rather than re-derived from mutable scenario config.
    - `analyzer_artifacts` object, required
    - `post_call_notes` string, nullable, required
    - `status` 'ongoing' | 'completed' | 'unanswered' | 'error': Call lifecycle state. - ONGOING: Call is currently in progress - COMPLETED: Call finished (regardless of whether it was answered) - ERROR: Technical error/exception during call processing - UNANSWERED: DEPRECATED - Use status=COMPLETED + telephony_result=NO_ANSWER instead. Kept for backwards compatibility with existing database records.
    - `telephony_result` 'connected' | 'busy' | 'no_answer' | 'rejected' | 'failed': Telephony-level call connection result. Represents what happened at the telephony level before any conversation. This is typically provided by the PBX/telephony system, not AI-classified.
    - `answered_by` 'human' | 'voicemail' | 'unclear': Who answered the call (AI-classified from transcript). This is the first stage of call classification - determining whether a human or voicemail answered the call.
    - `conversation_outcome` 'engaged' | 'refused' | 'wrong_person' | 'other': AI-classified judgement of what kind of conversation occurred. Only applicable when answered_by=HUMAN. Runtime actions such as scheduling a callback or transferring the caller are recorded independently in ``WorkflowDisposition`` and must never overwrite this judgement.
    - `workflow_disposition` 'none' | 'callback_scheduled' | 'transferred' | 'human_followup_required' | 'do_not_contact': Deterministic action the autonomous workflow took before finalizing. ``NONE`` is explicit for finalized autonomous calls with no special handoff/follow-up action. NULL means the dimension does not apply (human copilot, uploaded, or legacy calls), rather than overloading NULL with two meanings.
    - `agent_end_reason` 'conversation_complete' | 'existing_arrangement_claimed' | 'customer_requested_human' | 'identity_verification_failed' | 'customer_declined_to_engage' | 'customer_unavailable' | 'wrong_person' | 'do_not_contact' | 'hardship_detected' | 'dispute_raised' | 'escalation' | 'voicemail' | 'no_answer' | 'caller_hung_up' | 'caller_abandoned' | 'answer_failed' | 'answer_timeout' | 'dial_failed' | 'transfer_failed' | 'screening_timeout' | 'idle_timeout' | 'max_duration' | 'api_hangup' | 'error' | 'other': Normalized reason an autonomous call ended. The raw runtime string remains in ``analyzer_artifacts`` for diagnostics. This enum is the stable operational projection: semantic reasons exposed to the voice model plus bounded technical termination categories. Novel runtime endings degrade to ``OTHER`` instead of failing finalization.
    - `classification_final` boolean: Whether the call's terminal classification has been published (outcome_ready_at is set). The exact precondition for correcting answered_by / conversation_outcome — a call can be 'completed' while post-call facts are still finalizing, so clients must gate the correction UI on this rather than on status.
    - `counted_as` CallCountedAs: A call's classification and the stored fact it rests on.
      - `classification` 'contact' | 'contact_unknown' | 'attempt' | 'in_progress' | 'not_counted', required: What a call counts as for the frequency caps.
      - `basis` string, required: The stored fact behind the classification, in a few words.
    - `detected_scenario` string, nullable: AI-detected scenario from transcript content
    - `idv_outcome` 'verified' | 'partial' | 'incomplete': Identity-verification outcome for a call, denormalized from the IDV analyzer. Only set when IDV was required and a verdict was produced. A NULL column can mean IDV was not applicable, is still pending, or no verdict was available; API consumers use ``idv_required`` plus call status to distinguish those states. Derived from the terminal `identity_verification_state["status"]`: - VERIFIED: status `complete` — identity confirmed per the scenario's policy. - PARTIAL: at least one identity factor was confirmed, but the scenario's completion rule was never satisfied. - INCOMPLETE: required, and not a single factor was confirmed. PARTIAL exists because "not verified" covers two operationally different calls: one where the customer engaged with verification and it ran out of road, and one where it never started. Only the second is a candidate for "the agent didn't try". Note this is *tracking* state, not an adherence grade — the leadership IDV rate deliberately reads the reviewed QA scorecard instead (see `shared.idv`), because a human reviewer can correct it and the analyzer cannot. There is deliberately no `skipped` outcome. The live state machine still has a `skipped` terminal so the analyzer stops firing on a call that is not going to verify, but as a *result* it said nothing true that `incomplete` doesn't: verification was required and did not happen. It also read as a third, ambiguous colour on any report — neither pass nor fail — while the thing a reviewer actually needs to know (was skipping appropriate here?) is a transcript judgement the tracker never made.
    - `idv_source` 'live' | 'post_call' | 'inherited': Which mechanism produced the call's persisted IDV state. - LIVE: the in-call IDV analyzer's terminal state (the normal path). - POST_CALL: the post-call IDV judge — runs when the live analyzer never produced a state (e.g. no scenario reached the session) or the scenario was reclassified after the call, judging the full transcript against the resolved scenario's IDV rules. - INHERITED: the origin leg of a transfer verified the customer, and this recipient leg inherits that verdict — verification travels with the conversation, not the leg (see calls.idv_resolution). The verification exchange is on the origin leg's transcript, not this one's.
    - `idv_required` boolean, nullable: Whether the call's resolved scenario requires identity verification. NULL when the scenario record is unavailable.
    - `recording_disclosed` boolean, nullable: True when the IDV tracker positively captured the recording disclosure; otherwise NULL.
    - `scenario_id` string, uuid, nullable: FK to configured scenario
    - `was_transferred` boolean: Whether this call was transferred to/from another call
    - `transferred_from_call_id` string, uuid, nullable: If transferred, the call session this was transferred FROM
    - `transferred_to_call_id` string, uuid, nullable: If transferred, the call session this was transferred TO
    - `dashboard_open_percentage` number, nullable: Percentage of call duration with dashboard open (0-100)
    - `latency_summary_json` CallLatencySummary: Typed admin-only latency payload stored on a completed call.
      - `vaqi_l` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
        - `p50_ms` integer, nullable, required
        - `p95_ms` integer, nullable, required
        - `worst_ms` integer, nullable, required
        - `n` integer, required
      - `legs` SuggestedResponseLatencyLegs, required
        - `stt` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `commit` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `ttft` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `delivery` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
      - `autonomous_voice` AutonomousVoiceLatencySummary
        - `response_total` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `legs` SrcCallsSchemasAutonomousVoiceLatencyLegs, required: Stages of one spoken reply, in order; on a turn they sum to its total. The stages added after the first calls were recorded are optional, so the summaries stored on those calls still load.
          - `endpointing` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `turn_release` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `llm_ttft` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `tool` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `speech_release` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `tts_ttfb` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `transport` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
        - `turns` AutonomousVoiceTurnCounts, required
          - `total` integer, required
          - `complete` integer, required
          - `incomplete` integer, required
        - `by_kind` SrcCallsSchemasAutonomousVoiceLatencyByKind: End-to-end response time split by what answered the caller.
          - `plain` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `tool` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
          - `runtime` LatencyLegSummary, required: Percentiles for one latency leg and its contributing sample count.
            - `p50_ms` integer, nullable, required
            - `p95_ms` integer, nullable, required
            - `worst_ms` integer, nullable, required
            - `n` integer, required
        - `over_target` AutonomousVoiceOverTarget
          - `target_ms` integer, required
          - `replies` integer, required
          - `over` integer, required
        - `first_speech_ms` integer, nullable
        - `tool_handler` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `llm_thinking` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `llm_tokens_per_second` number, nullable
        - `unanswered_wait` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
        - `barge_in_stop` LatencyLegSummary: Percentiles for one latency leg and its contributing sample count.
          - `p50_ms` integer, nullable, required
          - `p95_ms` integer, nullable, required
          - `worst_ms` integer, nullable, required
          - `n` integer, required
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
  - `membership_reopened` boolean, required: Whether a campaign membership this outcome had closed was returned to in_progress. The campaign still needs activating before the next call goes out.
  - `cancelled_scheduled_calls` integer: Queued automated attempts cancelled because the corrected outcome is one the retry ladder stops on. Only unclaimed attempts are cancelled, so a call already being placed is never interrupted.
  - `membership_held_reason` string, nullable: Set when the campaign membership stayed closed even though the label that closed it was corrected away: 'attempts_exhausted' (the policy's attempt budget is spent) or 'commitment_still_refused' (the commitment classifier independently says the customer refused). Tells the reviewer why the account did not come back.
  - `invalidated_artifacts` string[]: Human-only artifacts cleared because the call is no longer marked human-answered (summary, coaching, tags, qa_scorecard, idv, ...)
  - `summary_regeneration_available` boolean: The call is now human-answered but carries a no-contact stub summary; retrying finalization would regenerate its artifacts.

## Other responses

- `422`: Validation Error

---

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