Skip to content
MeridFlow AiFlow v8.x • self-hosted

Data retention and deletion requests

Two complementary mechanisms for keeping AiFlow's stored call and message data bounded: an ongoing, automatic age-based purge configured by whoever operates your deployment, and a single on-demand endpoint for a specific "right to be forgotten" request tied to one person's phone number, email address, or Telegram chat.

On-demand deletion: DELETE /api/v1/data-subjects

Purges every Call (and its full transcript) and every OutboundMessage tied to a given phone number, email address, or Telegram chat id.

Authentication and roles

Takes an admin session token (see Authentication), not an API key. Requires Owner. This is the only endpoint on this site's admin surface reserved to Owner specifically, rather than Owner-or-Admin. It's the single most destructive, compliance-sensitive action in this API: it deletes real customer records with no undo.

Query parameters

Parameter Type Required Description
phone string At least one of the three Match on this exact phone number.
email string At least one of the three Match on this exact email address.
telegram_chat_id string At least one of the three Match on this exact Telegram chat id.

Any combination can be supplied together, and records matching any of them are purged. If none is supplied, this returns 400 Bad Request with {"detail": "Provide at least one of phone, email, or telegram_chat_id."}.

A Telegram send is addressed to a numeric chat id rather than a phone number or an email, so it is only reached by telegram_chat_id. Include it whenever a request covers someone your agents messaged on Telegram. No Call is ever addressed to a chat, so a telegram_chat_id on its own deletes messages and leaves calls alone.

curl -X DELETE "https://api.your-domain.com/api/v1/data-subjects?phone=%2B15557654321" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.delete(
    "https://api.your-domain.com/api/v1/data-subjects",
    headers={"Authorization": f"Bearer {admin_token}"},
    params={"phone": "+15557654321"},
)
response.raise_for_status()
result = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/data-subjects?phone=" +
    encodeURIComponent("+15557654321"),
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${adminToken}` },
  },
);
const result = await response.json();

Returns 200 OK:

{ "calls_deleted": 3, "outbound_messages_deleted": 1 }
Field Type Description
calls_deleted integer Number of Call rows deleted.
outbound_messages_deleted integer Number of OutboundMessage rows deleted (WhatsApp, Telegram, and email sends all live in this same table).

Exactly what this deletes

  • Calls: every Call row where phone_number equals the given phone, or visitor_email equals the given email (a call matches on either condition if both query parameters were supplied). Deleting a Call also deletes every Turn row belonging to it, its full transcript. There is no way to delete a call's turns independently, or to keep the transcript after the call record is gone.
  • Outbound messages: every OutboundMessage row whose to_address equals any value given. to_address is a single shared column holding a phone number for a WhatsApp send, an email address for an email send, and a chat id for a Telegram send, so all three parameters check the same column, just against different values.
  • An audit log entry is written for the deletion (action: "delete", resource_type: "data_subject", resource_id: null since the action spans a set of records rather than one). detail contains the phone or email you passed, plus the two counts above.

What this does not delete

Event payloads are not scanned or purged

This endpoint never touches the Event table. An event's payload is arbitrary JSON your own systems posted (see Sending events), with no schema AiFlow controls or can reliably interpret. It isn't scanned for a matching phone number or email address, even if the data you're trying to purge originally arrived inside one. The Event row that originally caused a now-deleted Call to be queued is untouched by this endpoint: it's governed only by the time-based EVENT_LOG_RETENTION_DAYS setting below, not by identity. If your compliance requirements cover inbound event payloads specifically, scrub personal data out of them at your own source system before posting to AiFlow. This API has no mechanism to do that for you after the fact.

Ongoing retention: environment variables, or live from the dashboard

Independent of the on-demand endpoint above, whoever operates your AiFlow deployment can configure automatic, age-based purging so records don't accumulate forever by default. Each setting below starts from an environment variable. A live override saved via GET or PATCH /api/v1/settings/operational (or Settings → Retry & retention, Owner or Admin only) takes over from it afterward, with no restart needed. GET always reflects the effective value, whichever one is actually in force:

Setting Env var Type Default Effect
call_transcript_retention_days CALL_TRANSCRIPT_RETENTION_DAYS integer unset (keep forever) Automatically deletes Call records (and their Turn transcripts) older than this many days.
event_log_retention_days EVENT_LOG_RETENTION_DAYS integer unset (keep forever) Automatically deletes Event records older than this many days.
outbound_message_retention_days OUTBOUND_MESSAGE_RETENTION_DAYS integer unset (keep forever) Automatically deletes OutboundMessage records older than this many days.

Each is independent: a deployment can set any combination of the three, or none of them. Leaving one unset (on the environment-variable side) or null (on the override side) keeps that record type indefinitely. There is no single master switch: each retention window is controlled independently. This is the mechanism that ages out an Event row over time, even though the on-demand endpoint above cannot target one by phone or email. Purging happens on an hourly tick, not the instant a record crosses the cutoff. A saved override applies starting from the next tick.

PATCH /api/v1/settings/operational also covers a fourth, non-retention setting, outbound_call_max_retries, in the same request: the endpoint always replaces all four values together, matching the dashboard's single combined form, not a per-field partial update.

curl -X PATCH https://api.your-domain.com/api/v1/settings/operational \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
    "outbound_call_max_retries": 3,
    "call_transcript_retention_days": 365,
    "event_log_retention_days": 90,
    "outbound_message_retention_days": 90
  }'
{
  "outbound_call_max_retries": 3,
  "call_transcript_retention_days": 365,
  "event_log_retention_days": 90,
  "outbound_message_retention_days": 90
}

Which one to use

Use the on-demand endpoint when someone has made a specific deletion request: you know their phone number or email and need their records gone now, on your own schedule, independent of any configured retention window. Use the retention-window settings above for the ongoing baseline: data you don't have a standing reason to keep past a certain age, purged automatically without anyone having to remember to call this endpoint.

Next

  • Audit log: confirm a deletion request was actually carried out, and by whom.
  • Sending events: where Event.payload (out of scope for the on-demand endpoint above) originates.