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

# Data retention policy

> How long OpenCX keeps each category of data, what triggers disposal, and how a deletion request cascades to backups, derivatives, and sub-processors.

export const DeletionCascadeFlow = () => {
  const SRC_X = 18;
  const SRC_W = 152;
  const SRC_H = 76;
  const COL_X = 288;
  const COL_W = 440;
  const ROW_H = 46;
  const ROW_TOP = 30;
  const ACCENT = '#16a34a';
  const TARGETS = [{
    label: 'Primary data store',
    sla: 'immediate',
    note: 'row-level delete'
  }, {
    label: 'Backups & point-in-time recovery',
    sla: '≤ 30 days',
    note: 'purged at next cycle'
  }, {
    label: 'Vector embeddings & derivatives',
    sla: '≤ 30 days',
    note: 'rebuilt without source'
  }, {
    label: 'Downstream replicas',
    sla: 'tracked',
    note: 'documented ticket per replica'
  }, {
    label: 'Sub-processors',
    sla: '≤ 30 days',
    note: 'contractually bound'
  }];
  const HEIGHT = ROW_TOP + TARGETS.length * ROW_H + 46;
  const rowY = i => ROW_TOP + i * ROW_H;
  const SRC_Y = (HEIGHT - 40 - SRC_H) / 2 + 8;
  const SRC_CY = SRC_Y + SRC_H / 2;
  return <div className="deletion-cascade not-prose my-8 w-full">
      <div className="relative w-full overflow-hidden rounded-md border border-black/10 bg-white px-4 py-5 dark:border-white/10 dark:bg-zinc-950">
        <svg viewBox={`0 0 800 ${HEIGHT}`} preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" className="block w-full" role="img" aria-label="A single deletion request cascades to five destinations: the primary data store immediately, backups and point-in-time recovery within 30 days at the next cycle, vector embeddings and derivatives within 30 days, downstream replicas via a tracked ticket per replica, and sub-processors within 30 days under contract. The whole cascade completes and is confirmed within the contractual window, 30 days by default.">
          {}
          {TARGETS.map((t, i) => {
    const y = rowY(i) + ROW_H / 2 - 4;
    const d = `M ${SRC_X + SRC_W},${SRC_CY} C ${SRC_X + SRC_W + 70},${SRC_CY} ${COL_X - 70},${y} ${COL_X - 8},${y}`;
    return <path key={`beam-${t.label}`} d={d} fill="none" stroke={ACCENT} strokeOpacity="0.4" strokeWidth="1.5" strokeLinecap="round" />;
  })}

          {}
          <g>
            <rect x={SRC_X} y={SRC_Y} width={SRC_W} height={SRC_H} rx="8" fill={ACCENT} fillOpacity="0.07" stroke={ACCENT} strokeOpacity="0.5" strokeWidth="1" />
            <text x={SRC_X + SRC_W / 2} y={SRC_Y + 30} textAnchor="middle" className="fill-zinc-900 dark:fill-zinc-100" style={{
    font: '600 12.5px ui-sans-serif, system-ui, sans-serif'
  }}>
              Deletion request
            </text>
            <text x={SRC_X + SRC_W / 2} y={SRC_Y + 48} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: '400 10px ui-sans-serif, system-ui, sans-serif'
  }}>
              contract end · customer
            </text>
            <text x={SRC_X + SRC_W / 2} y={SRC_Y + 61} textAnchor="middle" className="fill-zinc-500 dark:fill-zinc-400" style={{
    font: '400 10px ui-sans-serif, system-ui, sans-serif'
  }}>
              request · data subject
            </text>
          </g>

          {}
          {TARGETS.map((t, i) => {
    const y = rowY(i);
    return <g key={t.label}>
                <rect x={COL_X} y={y} width={COL_W} height={ROW_H - 8} rx="6" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
                <circle cx={COL_X + 16} cy={y + (ROW_H - 8) / 2} r="3.5" fill={ACCENT} fillOpacity="0.85" />
                <text x={COL_X + 30} y={y + 17} className="fill-zinc-800 dark:fill-zinc-100" style={{
      font: '500 11.5px ui-sans-serif, system-ui, sans-serif'
    }}>
                  {t.label}
                </text>
                <text x={COL_X + 30} y={y + 30} className="fill-zinc-400 dark:fill-zinc-500" style={{
      font: '400 10px ui-sans-serif, system-ui, sans-serif'
    }}>
                  {t.note}
                </text>

                {}
                <g>
                  <rect x={COL_X + COL_W - 96} y={y + 9} width="82" height="20" rx="10" fill={ACCENT} fillOpacity="0.1" />
                  <text x={COL_X + COL_W - 55} y={y + 22.5} textAnchor="middle" fill={ACCENT} style={{
      font: '600 10px ui-sans-serif, system-ui, sans-serif'
    }}>
                    {t.sla}
                  </text>
                </g>
              </g>;
  })}

          {}
          <g>
            <path d={`M ${COL_X + COL_W + 14},${rowY(0) + 2} h 8 V ${rowY(TARGETS.length - 1) + ROW_H - 10} h -8`} fill="none" className="stroke-black/20 dark:stroke-white/20" strokeWidth="1" />
            <text x={COL_X + COL_W + 30} y={(rowY(0) + rowY(TARGETS.length - 1) + ROW_H) / 2} className="fill-zinc-400 dark:fill-zinc-500" style={{
    font: '600 10px ui-sans-serif, system-ui, sans-serif'
  }} transform={`rotate(90 ${COL_X + COL_W + 30} ${(rowY(0) + rowY(TARGETS.length - 1) + ROW_H) / 2})`} textAnchor="middle">
              CONFIRMED ≤ 30 DAYS
            </text>
          </g>

          {}
          <line x1={SRC_X} y1={HEIGHT - 30} x2={COL_X + COL_W} y2={HEIGHT - 30} className="stroke-black/10 dark:stroke-white/10" strokeWidth="1" />
          <text x={SRC_X} y={HEIGHT - 13} className="fill-zinc-400 dark:fill-zinc-500" style={{
    font: '400 10px ui-sans-serif, system-ui, sans-serif'
  }}>
            Every leg produces a disposal record: asset, trigger, method, approver, completion time.
          </text>
        </svg>
      </div>
    </div>;
};

export const RetentionLifecycleFlow = () => {
  const STAGE_W = 132;
  const STAGE_H = 58;
  const GAP = 22;
  const X0 = 24;
  const BASELINE = 138;
  const ACCENT = '#16a34a';
  const HOLD = '#d97706';
  const STAGES = [{
    key: 'classify',
    title: 'Classify',
    body: 'Every asset gets a class and a retention rule at creation.'
  }, {
    key: 'retain',
    title: 'Retain',
    body: 'Held only as long as the schedule and contract require.'
  }, {
    key: 'trigger',
    title: 'Trigger',
    body: 'Age, contract end, or a deletion request starts disposal.'
  }, {
    key: 'dispose',
    title: 'Dispose',
    body: 'Cryptographic erasure, or secure deletion where keys do not apply.'
  }, {
    key: 'evidence',
    title: 'Evidence',
    body: 'Asset, method, approver, and time recorded for audit.'
  }];
  const stageX = i => X0 + i * (STAGE_W + GAP);
  return <div className="retention-lifecycle not-prose my-8 w-full">
      <div className="relative w-full overflow-hidden rounded-md border border-black/10 bg-white px-4 py-5 dark:border-white/10 dark:bg-zinc-950">
        <svg viewBox="0 0 800 232" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" className="block w-full" role="img" aria-label="Data lifecycle: information is classified at creation, retained for its scheduled period, released by a trigger such as age, contract end, or a deletion request, disposed of by cryptographic erasure, and recorded as audit evidence. A legal hold suspends the pipeline before disposal until the hold is released.">
          <defs>
            <marker id="lifecycle-arrow" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
              <path d="M 0 0 L 10 5 L 0 10 z" fill={ACCENT} />
            </marker>
            <marker id="lifecycle-arrow-hold" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
              <path d="M 0 0 L 10 5 L 0 10 z" fill={HOLD} />
            </marker>
          </defs>

          {}
          {STAGES.slice(0, -1).map((s, i) => <line key={`c-${s.key}`} x1={stageX(i) + STAGE_W + 3} y1={BASELINE} x2={stageX(i + 1) - 7} y2={BASELINE} stroke={ACCENT} strokeOpacity="0.45" strokeWidth="1.5" markerEnd="url(#lifecycle-arrow)" />)}

          {}
          <g>
            <rect x={stageX(2) - 6} y="30" width={STAGE_W + 12} height="34" rx="6" fill={HOLD} fillOpacity="0.08" stroke={HOLD} strokeOpacity="0.45" strokeWidth="1" strokeDasharray="4 3" />
            <text x={stageX(2) + STAGE_W / 2} y="51" textAnchor="middle" fill={HOLD} style={{
    font: '600 11px ui-sans-serif, system-ui, sans-serif'
  }}>
              Legal hold — suspends disposal
            </text>
            <line x1={stageX(2) + STAGE_W / 2} y1="66" x2={stageX(2) + STAGE_W / 2} y2={BASELINE - STAGE_H / 2 - 8} stroke={HOLD} strokeOpacity="0.5" strokeWidth="1.5" strokeDasharray="4 3" markerEnd="url(#lifecycle-arrow-hold)" />
          </g>

          {}
          {STAGES.map((s, i) => {
    const x = stageX(i);
    const y = BASELINE - STAGE_H / 2;
    return <g key={s.key}>
                <rect x={x} y={y} width={STAGE_W} height={STAGE_H} rx="7" className="fill-white stroke-black/10 dark:fill-zinc-900 dark:stroke-white/10" strokeWidth="1" />
                {}
                <rect x={x} y={y} width="3" height={STAGE_H} rx="1.5" fill={ACCENT} fillOpacity="0.85" />
                <text x={x + 14} y={y + 20} className="fill-zinc-900 dark:fill-zinc-100" style={{
      font: '600 12px ui-sans-serif, system-ui, sans-serif'
    }}>
                  {s.title}
                </text>
                <foreignObject x={x + 13} y={y + 26} width={STAGE_W - 22} height={STAGE_H - 30}>
                  <div xmlns="http://www.w3.org/1999/xhtml" className="text-zinc-500 dark:text-zinc-400" style={{
      font: '400 9.5px/1.35 ui-sans-serif, system-ui, sans-serif'
    }}>
                    {s.body}
                  </div>
                </foreignObject>
                <text x={x + STAGE_W - 10} y={y + 20} textAnchor="end" className="fill-zinc-300 dark:fill-zinc-600" style={{
      font: '600 11px ui-sans-serif, system-ui, sans-serif'
    }}>
                  {String(i + 1).padStart(2, '0')}
                </text>
              </g>;
  })}

          {}
          <g>
            <line x1={X0} y1="200" x2={stageX(STAGES.length - 1) + STAGE_W} y2="200" className="stroke-black/10 dark:stroke-white/10" strokeWidth="1" />
            <text x={X0} y="218" className="fill-zinc-400 dark:fill-zinc-500" style={{
    font: '400 10px ui-sans-serif, system-ui, sans-serif'
  }}>
              Automated in code · verified by the asset owner at quarterly access review · sampled in internal audit
            </text>
          </g>
        </svg>
      </div>
    </div>;
};

export const RetentionScheduleChart = () => {
  const MIN_DAYS = 7;
  const MAX_DAYS = 2555;
  const PLOT_X0 = 236;
  const PLOT_X1 = 764;
  const PLOT_W = PLOT_X1 - PLOT_X0;
  const scale = days => {
    const clamped = Math.max(MIN_DAYS, Math.min(MAX_DAYS, days));
    const t = Math.log(clamped / MIN_DAYS) / Math.log(MAX_DAYS / MIN_DAYS);
    return PLOT_X0 + t * PLOT_W;
  };
  const CLASS_COLOR = {
    Confidential: '#16a34a',
    Internal: '#71717a',
    Restricted: '#d97706'
  };
  const ROWS = [{
    label: 'Conversation data',
    cls: 'Confidential',
    days: 90,
    note: '90d + contract',
    extends: true
  }, {
    label: 'Attachments & exports',
    cls: 'Confidential',
    days: 90,
    note: '90 days'
  }, {
    label: 'Knowledge base content',
    cls: 'Confidential',
    days: MAX_DAYS,
    note: 'service lifetime',
    open: true
  }, {
    label: 'Vector embeddings',
    cls: 'Confidential',
    days: 30,
    note: '30 days'
  }, {
    label: 'Database backups',
    cls: 'Confidential',
    days: 30,
    note: '30 days rolling'
  }, {
    label: 'Encryption keys (retired)',
    cls: 'Restricted',
    days: 30,
    note: '30-day window'
  }, {
    label: 'Authentication logs',
    cls: 'Internal',
    days: 365,
    note: '12 months'
  }, {
    label: 'System & application logs',
    cls: 'Internal',
    days: 365,
    note: '12 months'
  }, {
    label: 'Marketing & prospect data',
    cls: 'Internal',
    days: 730,
    note: '24 months'
  }, {
    label: 'Audit & compliance evidence',
    cls: 'Confidential',
    days: 2555,
    note: '7 years'
  }, {
    label: 'Billing & financial records',
    cls: 'Confidential',
    days: 2555,
    note: '7 years'
  }];
  const TICKS = [{
    days: 30,
    label: '30d'
  }, {
    days: 90,
    label: '90d'
  }, {
    days: 365,
    label: '1y'
  }, {
    days: 730,
    label: '2y'
  }, {
    days: 2555,
    label: '7y'
  }];
  const ROW_H = 30;
  const BAR_H = 11;
  const TOP = 64;
  const HEIGHT = TOP + ROWS.length * ROW_H + 34;
  return <div className="retention-chart not-prose my-8 w-full">
      <div className="relative w-full overflow-hidden rounded-md border border-black/10 bg-white px-4 py-5 dark:border-white/10 dark:bg-zinc-950">
        <svg viewBox={`0 0 800 ${HEIGHT}`} preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" className="block w-full" role="img" aria-label="Retention schedule by data category, on a logarithmic time axis. Vector embeddings, database backups and retired encryption keys are held 30 days; attachments and exports 90 days; conversation data 90 days plus any contractual extension; authentication, system and application logs 12 months; marketing and prospect data 24 months; audit evidence and billing records 7 years; knowledge base content for the service lifetime.">
          <defs>
            {}
            <linearGradient id="retention-open" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0%" stopColor={CLASS_COLOR.Confidential} stopOpacity="0.95" />
              <stop offset="55%" stopColor={CLASS_COLOR.Confidential} stopOpacity="0.5" />
              <stop offset="100%" stopColor={CLASS_COLOR.Confidential} stopOpacity="0" />
            </linearGradient>
          </defs>

          {}
          <g style={{
    font: '500 10.5px ui-sans-serif, system-ui, sans-serif'
  }}>
            {Object.entries(CLASS_COLOR).map(([name, color], i) => <g key={name} transform={`translate(${PLOT_X0 + i * 118} 18)`}>
                <rect x="0" y="-7" width="9" height="9" rx="2" fill={color} />
                <text x="15" y="1" className="fill-zinc-600 dark:fill-zinc-300">
                  {name}
                </text>
              </g>)}
            <text x="0" y="19" className="fill-zinc-400 dark:fill-zinc-500" style={{
    font: '500 10.5px ui-sans-serif, system-ui, sans-serif'
  }}>
              CLASSIFICATION
            </text>
          </g>

          {}
          <g>
            {TICKS.map(t => <g key={t.days}>
                <line x1={scale(t.days)} y1={TOP - 12} x2={scale(t.days)} y2={TOP + ROWS.length * ROW_H - 8} className="stroke-black/10 dark:stroke-white/10" strokeWidth="1" strokeDasharray="2 4" />
                <text x={scale(t.days)} y={TOP - 20} textAnchor="middle" className="fill-zinc-400 dark:fill-zinc-500" style={{
    font: '500 10px ui-sans-serif, system-ui, sans-serif'
  }}>
                  {t.label}
                </text>
              </g>)}
          </g>

          {}
          {ROWS.map((row, i) => {
    const y = TOP + i * ROW_H;
    const color = CLASS_COLOR[row.cls];
    const x1 = scale(row.days);
    const barW = Math.max(3, x1 - PLOT_X0);
    return <g key={row.label}>
                <text x="0" y={y + BAR_H / 2 + 1} className="fill-zinc-700 dark:fill-zinc-200" style={{
      font: '450 11.5px ui-sans-serif, system-ui, sans-serif'
    }}>
                  {row.label}
                </text>

                {}
                <rect x={PLOT_X0} y={y - 1} width={PLOT_W} height={BAR_H + 2} rx="3" className="fill-black/[0.03] dark:fill-white/[0.04]" />

                {}
                <rect x={PLOT_X0} y={y} width={barW} height={BAR_H} rx="2.5" fill={row.open ? 'url(#retention-open)' : color} fillOpacity={row.open ? 1 : 0.9} />

                {}
                {row.extends && <g>
                    <line x1={x1 + 3} y1={y + BAR_H / 2} x2={x1 + 46} y2={y + BAR_H / 2} stroke={color} strokeWidth="1.5" strokeDasharray="3 3" strokeLinecap="round" />
                    <path d={`M ${x1 + 46},${y + BAR_H / 2 - 3.5} L ${x1 + 52},${y + BAR_H / 2} L ${x1 + 46},${y + BAR_H / 2 + 3.5} Z`} fill={color} />
                  </g>}

                {}
                <text x="228" y={y + BAR_H / 2 + 1} textAnchor="end" className="fill-zinc-400 dark:fill-zinc-500" style={{
      font: '500 10px ui-sans-serif, system-ui, sans-serif'
    }}>
                  {row.note}
                </text>
              </g>;
  })}

          {}
          <line x1={PLOT_X0} y1={TOP + ROWS.length * ROW_H - 8} x2={PLOT_X1} y2={TOP + ROWS.length * ROW_H - 8} className="stroke-black/10 dark:stroke-white/10" strokeWidth="1" />
          <text x={PLOT_X0} y={HEIGHT - 8} className="fill-zinc-400 dark:fill-zinc-500" style={{
    font: '400 10px ui-sans-serif, system-ui, sans-serif'
  }}>
            Time from retention trigger · logarithmic scale
          </text>
        </svg>
      </div>
    </div>;
};

Retention is a security control, not a storage setting. Data that no longer serves a purpose is
liability without upside — so OpenCX holds every category of information for a defined period,
disposes of it by a defined method, and produces a record proving it happened.

This page is the customer-facing summary of the Open Data Management & Retention Policy
(DMR-001), owned by the CTO/CISO and reviewed annually. It aligns with ISO/IEC 27001:2022
controls A.5.33, A.8.10 and A.8.12, SOC 2 criteria C1.1–C1.2 and P4.1–P4.3, and the
storage-limitation principle in GDPR Article 5(1)(e).

<Note>
  Where your contract with OpenCX defines a retention period, **your contract governs**. The
  schedule below is the default applied in the absence of a contractual term, and it is a ceiling —
  asset owners may retain less, never more, without written CTO/CISO approval.
</Note>

## Principles

<CardGroup cols={2}>
  <Card title="Data minimisation" icon="filter">
    Information is retained only where there is a defined business, contractual, or legal purpose
    for keeping it.
  </Card>

  <Card title="Storage limitation" icon="hourglass-half">
    Retention is bounded by the purpose. When the purpose ends, the clock starts.
  </Card>

  <Card title="Auditable disposal" icon="clipboard-check">
    Every disposal produces a verifiable record — asset, trigger, method, approver, completion time.
  </Card>

  <Card title="Default delete" icon="trash">
    Where no explicit rule applies, data is treated as Internal and reviewed for deletion after
    12 months of inactivity.
  </Card>
</CardGroup>

## Retention schedule

<RetentionScheduleChart />

The same schedule in full, including the event that starts the retention clock and the method used
to dispose of the data:

| Category                            | Classification | Retention                                        | Retention trigger                        | Disposal method                         |
| ----------------------------------- | -------------- | ------------------------------------------------ | ---------------------------------------- | --------------------------------------- |
| Conversation data                   | Confidential   | Per contract — default active lifetime + 90 days | Contract termination or customer request | Cryptographic erasure                   |
| Attachments & exports               | Confidential   | Per contract — default 90 days                   | Contract termination                     | Cryptographic erasure                   |
| Knowledge base content              | Confidential   | Service lifetime                                 | Contract termination or source removal   | Cryptographic erasure                   |
| Vector embeddings (derivatives)     | Confidential   | 30 days after source deletion                    | Source deletion event                    | Cryptographic erasure                   |
| Database backups (PITR + snapshots) | Confidential   | 30 days rolling                                  | Age                                      | Automated overwrite + key rotation      |
| Authentication & authorisation logs | Internal       | 12 months                                        | Age                                      | Automated deletion from immutable store |
| System & application logs           | Internal       | 12 months                                        | Age                                      | Automated deletion                      |
| Compliance & audit evidence         | Confidential   | 7 years                                          | Regulatory / audit cycle                 | Secure deletion after retention         |
| Billing & financial records         | Confidential   | 7 years                                          | Tax / legal obligations                  | Secure deletion after retention         |
| Marketing & prospect data           | Internal       | 24 months, or on request                         | Inactivity or data-subject request       | Secure deletion                         |
| Encryption keys (decommissioned)    | Restricted     | 30-day waiting period, then destroyed            | Key revocation                           | Scheduled destruction, dual approval    |

<Tip>
  **Derivatives are on the clock too.** Vector embeddings built from your content are treated as
  Confidential and deleted within 30 days of the source being deleted — they do not outlive the data
  they were derived from.
</Tip>

## Lifecycle

Retention is enforced in code, not by reminder. Each asset carries a classification and a rule from
the moment it is created, and disposal runs automatically when the trigger fires.

<RetentionLifecycleFlow />

## Trigger-based removal

Not every retention clock should be a calendar. Often the right moment to strip a session is the
moment it stops being operationally useful — the ticket closed, the case was resolved in your
ticketing system, the thread went cold. **Workflows** let you bind removal to that event instead of
waiting out a fixed window.

The pattern is the same in every case: a trigger fires, a workflow runs, and the workflow calls
[`POST /redaction/session`](/api-reference/redaction/redact-session) to strip the session's messages
and attachments.

<Tabs>
  <Tab title="Ticket closed in your ticketing system">
    When an agent closes the ticket in Zendesk, Salesforce, HubSpot, or any connected ticketing
    system, the webhook closes the matching OpenCX session and fires the **`ticket-resolved`**
    trigger. Hang a workflow off it to remove the transcript as soon as the system of record says
    the work is done.

    <Steps>
      <Step title="Trigger: Ticket resolved">
        Fires on close in OpenCX **and** on close propagated from the connected ticketing system.
      </Step>

      <Step title="Optional: Wait">
        Add a **Wait** step for a cooling-off period — long enough to handle a reopen, short enough
        to beat the default window. A common choice is 7 days.
      </Step>

      <Step title="Optional: Condition">
        Branch on tags, team, or channel so removal only applies where you want it — for example
        sessions tagged `payments` but not those tagged `dispute`.
      </Step>

      <Step title="Action: Redact the session">
        An **HTTP call** step to `POST /redaction/session` with the session id from the trigger
        context. The API key needs the `redaction:write` scope.
      </Step>
    </Steps>

    <Warning>
      Redaction is irreversible. Put the reopen window before the removal step, not after — a
      session stripped the instant a ticket closes has no transcript left if the customer replies an
      hour later and the ticket reopens.
    </Warning>
  </Tab>

  <Tab title="Session went inactive">
    The **`ticket-inactive`** trigger fires when a session has had no activity for a configured
    period. Use it to catch sessions that were never formally closed — abandoned chats, one-message
    threads, bot-only conversations — which otherwise sit at full fidelity until the org-wide window
    expires.

    Pair it with a condition on message count or channel so you only sweep the genuinely
    low-value ones.
  </Tab>

  <Tab title="Tag-driven removal">
    The **`ticket-tag-added`** trigger turns removal into a one-click action for your agents. Add a
    tag such as `erasure-request` to a session and the workflow strips it immediately — useful when
    a customer asks for erasure mid-conversation and you want the agent to action it without filing
    a ticket with your security team.

    Gate this one carefully: anyone who can tag a session can trigger deletion. Restrict the tag to
    a role that should hold that power.
  </Tab>

  <Tab title="Scheduled sweep">
    A **`cron-trigger`** workflow runs on your schedule — nightly, weekly — and applies a rule the
    fixed retention window cannot express. For example: strip sessions from a specific channel after
    14 days while leaving everything else on the standard schedule, or remove test and internal
    sessions on a much shorter cycle than customer conversations.

    This is the right tool when the rule depends on data the retention schedule does not know about.
  </Tab>
</Tabs>

<Note>
  Trigger-based removal runs **in addition to** the org-wide automatic redaction window, which
  strips PII from closed sessions after a configurable period (30 days by default). Workflows let
  you act **earlier** or **more selectively** — they don't replace the baseline, and no workflow can
  extend retention beyond the schedule above.
</Note>

## Disposal methods

<AccordionGroup>
  <Accordion title="Cryptographic erasure" icon="key">
    The primary method for data held in cloud-managed services. The encryption keys protecting the
    data are scheduled for destruction with a 7–30 day waiting window and dual approval. Once the
    keys are destroyed, the ciphertext is permanently unreadable — including any copy that exists in
    a backup taken before the deletion.

    Applied to customer data on contract termination, to decommissioned backups, and to derivative
    datasets such as vector embeddings.
  </Accordion>

  <Accordion title="Secure logical deletion" icon="eraser">
    For information that cannot be cryptographically erased — for example shared corporate records
    in collaboration tools — deletion follows NIST SP 800-88 Rev. 1. Records are removed from the
    primary store, purged from backups at the next backup cycle, and removed from downstream
    replicas under a documented ticket.
  </Accordion>

  <Accordion title="Physical media" icon="hard-drive">
    OpenCX operates no customer-facing physical media. Corporate endpoints are full-disk encrypted;
    on retirement the disk is cryptographically erased through device management and the hardware is
    wiped or physically destroyed by a contracted provider under certificate of destruction.
  </Accordion>

  <Accordion title="Evidence of disposal" icon="file-check">
    Every disposal action produces an auditable record: the asset identifier, the trigger event, the
    method, the approving party, and the completion time. Disposal is verified by the asset owner at
    the next quarterly access review and sampled by the security function during internal audit.
  </Accordion>
</AccordionGroup>

## Deletion requests

A deletion request is not a single `DELETE`. A copy of a record can exist in the primary store, in
backups, in derived datasets, and at a sub-processor — so the request fans out to every one of them
and is confirmed only when the last leg completes.

<DeletionCascadeFlow />

**Where OpenCX is a Processor** — your customer data — deletion requests are executed within the
contractual timeframe, 30 days by default, coordinated across the primary store, backups, derivative
datasets, and relevant sub-processors.

**Where OpenCX is a Controller** — employee and prospect data — data-subject requests are logged,
authenticated, actioned, and confirmed in writing to the requester under applicable data-protection
law.

<Card title="Submit a deletion or data-subject request" icon="envelope" href="mailto:security@open.cx">
  Email [security@open.cx](mailto:security@open.cx). Include the org, the scope of data, and the
  legal basis if the request is made on behalf of a data subject.
</Card>

## Legal holds & exceptions

A legal hold suspends normal retention and disposal for specified data until the hold is released.

<Steps>
  <Step title="Initiated by an authorised party">
    Only the CEO, the CTO/CISO, or General Counsel may place a hold.
  </Step>

  <Step title="Documented in writing">
    Scope and justification are recorded before the hold takes effect.
  </Step>

  <Step title="Reviewed at least every six months">
    Holds are re-justified or released — they do not become permanent by neglect.
  </Step>
</Steps>

Exceptions to the schedule — for example extended retention for a specific category — require
written CTO/CISO approval, are recorded in the GRC platform, and are subject to periodic review.

## Sub-processors

Sub-processors are contractually required to meet retention periods and disposal rules **at least as
strict** as those above. Compliance is verified annually under the Vendor Management Policy, and on
any change of scope.

## Governance

| Role                    | Responsibility                                                                           |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| CTO / CISO              | Owns the policy, approves exceptions, authorises legal holds, escalates breaches         |
| Data Protection Officer | Data-protection compliance, data-subject rights, sub-processor agreements                |
| Engineering             | Implements retention rules in code, enforces automated disposal, maintains audit records |
| Asset owners            | Identify applicable periods for their assets and sign off on disposal                    |
| People Operations       | Employee and contractor records, in line with employment law                             |
| All personnel           | Apply classification and never retain data beyond its defined period                     |

## Retention you control

Several retention windows are configurable per organisation, so you can hold data for less time than
the default without a contract change:

<CardGroup cols={2}>
  <Card title="PII redaction window" icon="user-shield" href="/safety/pii-redaction">
    Sensitive values are redacted from conversations after session closure — 30 days by default,
    configurable for your org.
  </Card>

  <Card title="Connect thread lifecycle" icon="timer" href="/connect/threads">
    Grace and retention windows for Connect threads are set per org, and per thread at creation.
  </Card>
</CardGroup>

<Info>
  Enterprise customers can request the full signed policy (DMR-001), the sub-processor register, and
  disposal evidence samples as part of a security review. Contact
  [security@open.cx](mailto:security@open.cx).
</Info>
