Security
Every workspace runs on the same security posture. Here is what protects your data today.
- Tenant isolation. Each organization's data is separated by row-level security, and the backend re-checks access on every request. One workspace cannot read another workspace's records.
- Append-only audit trail. Every decision, signal, and override is written to an audit trail that cannot be edited or deleted. Each entry carries a correlation ID, so a case can be traced from start to finish.
- No raw card data. Lumtry never stores card numbers. Refunds and charges reference a processor token instead, so card data stays with the payment processor.
- Encrypted in transit. Traffic to Lumtry is encrypted in transit, and the dashboard sends strict security headers, including a content security policy and HSTS.
- Verified webhooks. Every incoming webhook, whether from your store, your payment processor, or Slack, is signature-verified before Lumtry writes anything to your data. A webhook with an invalid signature is rejected.
How Lumtry uses AI
- A deterministic policy that your organization writes and versions decides each case. A refund already in progress stays pinned to the policy version it started under.
- AI reasoning is one input to that policy, never the decision itself. It scores ambiguous cases and writes a rationale, scoped to what the policy already allows.
- A person approves any case the policy does not clear on its own, from the dashboard or from Slack, before anything executes.
- Shopper-facing text that AI helps draft is marked as AI-generated by construction, unless a person edits it and sends it under their own name, so you can tell which words came from a person and which came from the model.
- Shoppers can request a human review of their case from the return status page. A contested case shows one of three states: awaiting review, reviewed and upheld, or reviewed and changed.
Lumtry does not publish an accuracy or performance figure for AI-assisted decisions, because the policy, not the model, decides.
See our sub-processorsSee how data flows through the Service
Sub-processors
Lumtry works with the providers below to run parts of the Service. Each entry shows its role, status, and what we can verify today.
Anthropic
Live| Field | Value | Source |
|---|---|---|
| Purpose | Model inference for agent runs on every route class ( | code |
| Region of processing | Anthropic direct API, US processing. No EU-resident door exists at launch (owner decision 2026-09-12, epic #597: first markets are US and Canada). | code + decision |
| Data categories | Redacted case text (refund reasons, portal notes, shopper communications), merchant policy text, evidence-pack sections, case timelines, aggregated abuse-risk and sentiment signals, offer parameters, agent outputs under review. Never: processor tokens, card data, full email local-parts, API keys, request headers. | code |
| Retention on the provider side | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Zero-data-retention / training opt-out | Repo record (memo §8): no-training default for API traffic; Zero Data Retention available on request. Under verification We have not yet confirmed this value. Check back as we complete verification. | owner |
| Contractual basis | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Date last verified | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
OpenAI
Live| Field | Value | Source |
|---|---|---|
| Purpose | Model inference for agent runs on every route class where the tenant's routing policy selects an OpenAI model; the batch lane (judge only) and prompt caching are enabled for this endpoint. | code |
| Region of processing | OpenAI default API endpoint, US processing. An EU-region endpoint exists on the provider side but is not configured (no EU door at launch, epic #597). | code + decision |
| Data categories | Same categories as Anthropic (the router sends the same redacted wire to whichever endpoint the policy selects). | code |
| Retention on the provider side | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Zero-data-retention / training opt-out | Repo record (memo §8): API traffic not used for training by default Under verification We have not yet confirmed this value. Check back as we complete verification. | owner |
| Contractual basis | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Date last verified | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
Mistral
Live| Field | Value | Source |
|---|---|---|
| Purpose | Model inference for agent runs on every route class where the tenant's routing policy selects a Mistral model. No batch lane, no prompt caching, no extended thinking on this endpoint (registry capabilities are empty), so no cached prefixes exist at the provider. | code |
| Region of processing | Mistral hosted API, EU-default hosting (memo §8). | repo record |
| Data categories | Same categories as Anthropic. | code |
| Retention on the provider side | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Zero-data-retention / training opt-out | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Contractual basis | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Date last verified | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
Vercel AI Gateway
ConditionalNot a sub-processor by default. The gateway host is admitted by the router only when EQUALI_VERCEL_AI_GATEWAY_ENABLED is true and the operator declares explicitly marked gateway entries in EQUALI_ROUTER_ENDPOINTS (ADR-0089; GTM plan §15). The default is off and GA does not depend on it. When an operator enables it, this section becomes a live row and must be completed before the flag is flipped:
| Field | Value | Source |
|---|---|---|
| Purpose | Transport proxy in front of an upstream model provider (one of the three above, or another declared endpoint). Never on the batch lane. | code |
| Region of processing | Vercel infrastructure (US/global); the upstream provider's own region still applies behind it. | code |
| Data categories | Identical to whichever upstream provider the gateway entry fronts; the gateway sees the full redacted wire. | code |
| Retention on the provider side | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Zero-data-retention / training opt-out | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Contractual basis | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Date last verified | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Field | Value | Source |
|---|---|---|
| Purpose | Model inference for agent runs on every route class where the tenant's routing policy selects a Google model, once cleared. Prompt caching is enabled for this endpoint (no batch lane). | code |
| Region of processing | Google's default Gemini API endpoint, US processing. No EU-resident door exists at launch (epic #597: first markets are US and Canada). | code + decision |
| Data categories | Same categories as Anthropic (the router sends the same redacted wire to whichever endpoint the policy selects). | code |
| Retention on the provider side | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Zero-data-retention / training opt-out | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Contractual basis | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Date last verified | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
xAI
Pending clearance| Field | Value | Source |
|---|---|---|
| Purpose | Model inference for agent runs on every route class where the tenant's routing policy selects an xAI model, once cleared. Prompt caching is enabled for this endpoint (no batch lane). | code |
| Region of processing | xAI's default API endpoint (gRPC transport), US processing. No EU-resident door exists at launch (epic #597: first markets are US and Canada). | code + decision |
| Data categories | Same categories as Anthropic (the router sends the same redacted wire to whichever endpoint the policy selects). | code |
| Retention on the provider side | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Zero-data-retention / training opt-out | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Contractual basis | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
| Date last verified | Under verification We have not yet confirmed this value. Check back as we complete verification. | Under verification |
Where this is documented
These policies and pages reference the same sub-processors.
| Document | What it covers |
|---|---|
| Supabase (Postgres, Auth) | The source rows the data blocks are built from, agent run records, invocation rows with token counts and cost, injection-screen records and hashes of untrusted inputs (never prompt text). |
| Temporal Cloud | Workflow histories for agent runs: activity inputs/outputs, which include the composed prompt blocks and model outputs for the history retention period. |
| Logfire (US sink) | Traces and metrics for every model call: token counts, cost, route decision, redaction counts. Prompt/completion content only when both the router flag and the tenant agents.telemetry.content_capture flag are on. |
| Deploy target (AWS / Azure / GCP / managed) | Runs the router, worker and API containers that hold provider keys in the platform secrets manager; no AI data at rest beyond container logs (redacted). |
| Payment processors, store platforms, Slack | Not AI sub-processors: no model output is sent to them and no AI provider receives their raw payloads (processor payloads are a declared untrusted input of the remediation proposer but reach the model only as normalised status rows, never as the raw webhook body). |
How data flows
This section explains what leaves Lumtry's systems, where it goes, and what stays inside.
How a request leaves Lumtry
- A worker activity (or an API service) composes the prompt from labelled blocks. Each block is a
DataBlock(source, text, trusted);build_promptrenders it on the wire as<data source="<label>" trusted="true|false">…</data>so the model, the redaction layer and the compliance record all see the same label. Trusted blocks are merchant- or platform-sourced (policy text, ids, aggregates); untrusted blocks are shopper-authored free text and prior model output. - Untrusted blocks are screened first where the agent declares them (
AgentSpec.untrusted_inputs). The injection screen runs the keyword rules and, when the tenant's screen settings allow it, onescreen_injectionmodel call whose wire wraps the text as<untrusted_data source="<label>">…</untrusted_data>; that call uses the same router route as every agent call (POST /v1/agents/{agent_id}/model-request), so step 3 applies to it too. A flagged block is withheld from the author agent (dispute reads return empty rows; the case note agent runs without the history block). The screen record stores a hash of each untrusted input, never the text. - The router redacts once, before any dispatch (,
redact_agent_messages). Every string on the wire - user prompt, every data block, every tool return, every prior model turn on multi-step runs - passes the universalredact_stringbaseline from: - secret shapes (provider keys, bearer tokens, webhook secrets) →
[REDACTED]; - Luhn-valid 13-19 digit sequences (card numbers) →
[REDACTED]; - email addresses → the literal
[EMAIL]@[REDACTED](the whole address, local-part and domain, is replaced; nothing of the address survives). Blocks whose label appears in the tenant policy'sagents.pii.redact_by_sourcemap are additionally routed through the field-wiseredact_blockhints for that label. The default map is{}(migration 122), so at launch the baseline is what every block gets. - Dispatch goes to the endpoint the tenant's routing policy selects among the sub-processors (Anthropic, OpenAI, Mistral, Google, xAI, plus the optional Vercel AI Gateway transport); the same redacted wire goes to whichever endpoint wins, regardless of vendor. Google and xAI carry
compliance_cleared=falsein the shipped platform policy (feature 581), so the routing policy and publish gate both refuse to select either vendor for production agent traffic until an operator records that vendor's sub-processor row and flips the flag; the tables below describe what would be sent, not that it is currently sent. - Model output comes back into the worker activity, is validated against the agent's typed output, and is persisted as a proposal or draft. Output never reaches a processor, store platform or Slack unreviewed; it is text or a proposal row an operator acts on.
Identifiers we pseudonymize
Only platform-issued UUIDs cross: refund_case_id, dispute_case_id, policy_version_id, agent_run_id, discrepancy and execution row ids. They identify a row inside the tenant's own data and carry no shopper identity. The correlation id is an internal header between worker, router and API; it is not placed in the prompt and is not sent to the provider. Email addresses inside free text are not pseudonymised but removed outright ([EMAIL]@[REDACTED]), so no address-derived pseudonym reaches a provider either.
What never leaves Lumtry
| Field | Why it cannot reach a provider |
|---|---|
Processor tokens, payment-method ids, external_reversal_reference bodies | No block reads them; the remediation execution view exposes only status, amount, method and an opaque reference id. Secret-shaped strings are additionally scrubbed by the baseline. |
| Raw card data / PAN | Never stored (CLAUDE.md security must-haves); any Luhn-valid 13-19 digit run that appears in free text is replaced by [REDACTED] before dispatch. |
| Full email local-part (and the domain) | Baseline replaces every email address on every wire string with [EMAIL]@[REDACTED]. |
| API keys, bearer tokens, webhook secrets, request headers | Secret-shape regexes on every wire string; the observability layer's denied header set strips them from any logged request; provider credentials live only in router scope (ADR-0004). |
| Raw processor / store webhook payloads | The remediation proposer declares processor_payloads as an untrusted input class, but its read tools return normalised rows (remediation.execution_status etc.), never the stored webhook body. |
Shopper account identifiers (external_customer_id, order emails, addresses) | Used server-side as lookup keys (prior-transaction summary, order lookup) and never rendered into a block; the summary sent is a count, first/last date and total. |
| Prompt and completion text into telemetry | Off unless both the router flag and the tenant flag are on (§4). |
Route classes
Route class screen
| Agent | Block / input | Trusted | Content | Redaction before the call |
|---|---|---|---|---|
screen_injection | screened_text (declared input); on the wire the label is the block being screened: case_history, shopper_communication, or a remediation read-tool id (remediation.case_timeline, remediation.execution_status, remediation.discrepancy, remediation.audit_trail, remediation.pinned_policy, remediation.compensation_trace) | no | The untrusted text the author agent is about to receive, wrapped as <untrusted_data source=…>. | Baseline (the screen call goes through the router like any agent call). |
Route class classify
| Agent | Block / input | Trusted | Content | Redaction before the call |
|---|---|---|---|---|
classify_refund_case | refund_reason_text | no | refund_cases.refund_reason free text, stripped. | Baseline; redact_by_source hint if configured. |
classify_refund_case | customer_locale | yes | refund_cases.customer_locale (BCP-47 tag or en-US). | None needed. |
resolve_sentiment | message_text | no | Composed from portal_submissions.note and refund_cases.refund_reason. | Baseline; redact_by_source hint if configured. |
resolve_sentiment | customer_locale | yes | As above. | None needed. |
narrate_abuse_risk | refund_case | yes | A summary composed from abuse_risk_assessments.features (bands, counts, signal names) plus the recorded decision line. No shopper free text. | Baseline. |
retention_offer_copy | brand_name, order_number, offers, currency, locale | yes | Merchant brand name, the order number, offer parameters (type, bonus/partial percent) as JSON, ISO currency, locale. No shopper free text (the agent declares no untrusted inputs). | Baseline. |
Route class author
| Agent | Block / input | Trusted | Content | Redaction before the call |
|---|---|---|---|---|
case_note_reference | refund_case | yes | The refund_case_id only. | None needed. |
case_note_reference | case_history | no | portal_submissions.note + refund_cases.refund_reason, screened first; withheld when flagged. | Injection screen, then baseline; redact_by_source hint if configured. |
dispute_evidence_narrative | shopper_communication (tool dispute.shopper_communications) | no | portal_submissions.note with created_at, screened first; empty rows when flagged. | Injection screen, then baseline (tool returns are wire strings). |
dispute_evidence_narrative | evidence_sections (tool dispute.evidence_sections) | yes | dispute_evidence_packs.sections_jsonb: per-section status, source names and merchant-assembled facts. | Baseline. |
dispute_evidence_narrative | case_timeline (tool dispute.case_timeline) | yes | audit_events category, timestamp and summary for the case. | Baseline. |
dispute_evidence_narrative | policy_version_text (tool dispute.policy_version_text) | yes | Pinned policy version: rule names, outcomes, explanation templates, published date. | Baseline. |
dispute_evidence_narrative | prior_transaction_summary (tool dispute.prior_transaction_summary) | yes | Count, first/last timestamp, total in minor units and currency of the shopper's prior cases; the customer id is the lookup key and is not sent. | Baseline. |
remediation_proposer | initial prompt | yes | refund_case_id and, for discrepancy targets, discrepancy_id, stated as plain ids. | None needed. |
remediation_proposer | processor_payloads, shopper_text, case_notes (declared input classes) | no | Reached only through the read tools below; each tool return is screened as a block labelled with the tool id and withheld when flagged. | Injection screen, then baseline. |
remediation_proposer | remediation.case_timeline, remediation.audit_trail | screened | audit_events id, category, timestamp, summary. | Injection screen, then baseline. |
remediation_proposer | remediation.execution_status | screened | Execution rows: id, status, amount in minor units, currency, method, opaque reversal reference; processor view status only. | Injection screen, then baseline. |
remediation_proposer | remediation.discrepancy | screened | Discrepancy id, status, kind, amount, currency. | Injection screen, then baseline. |
remediation_proposer | remediation.pinned_policy | screened | Pinned policy text (rule names/outcomes). | Injection screen, then baseline. |
remediation_proposer | remediation.compensation_trace | screened | Compensation step name, status, attempt timestamp. | Injection screen, then baseline. |
Route class critic
| Agent | Block / input | Trusted | Content | Redaction before the call |
|---|---|---|---|---|
dispute_narrative_critic | draft_narrative | no | The author agent's draft narrative (prior model output, treated as untrusted by the family rule). | Baseline. |
Route class reason
| Agent | Block / input | Trusted | Content | Redaction before the call |
|---|---|---|---|---|
propose_policy_rules | task_input | yes | JSON of the merchant's pasted policy text, workspace currency and locale (policy import). | Baseline. |
suggest_policy_rules | task_input | yes | JSON of channel scope, channel roster, currency, lookback window, current rule summaries and evidence groups (aggregate bands only; no case free text, no shopper ids). | Baseline. |
policy_backtest_explainer | get_backtest_summary (tool policy_backtest.get_backtest_summary) | yes | This run's stored backtest summary (policy_backtests.summary_jsonb): case counts, transitions, approval-queue load before/after, and per-rule attribution, all computed by the pure equali_policy_engine.backtest evaluator before the run starts. No shopper free text. | Baseline. |
policy_backtest_explainer | draft_rule_text (tool policy_backtest.get_draft_rules) | no | A draft rule's name and explanation_template: merchant-authored free text. Every other field the same tool returns for a rule (rule_id, condition_json, outcome, priority) is a platform value the pure evaluator already validated. | Baseline. |
Route class judge
| Agent | Block / input | Trusted | Content | Redaction before the call |
|---|---|---|---|---|
eval_judge | rubric | yes | The per-agent grading rubric (platform text). | None needed. |
eval_judge | sampled_output | no | The sampled agent run's output and the facts it was produced from, as already redacted for that run. | Baseline (runs on the batch lane where the endpoint supports it). |
Telemetry
Every model call emits a span and an invocation row with token counts, cost, route decision, endpoint, effort and redaction counters. Prompt and completion content is attached only when both RouterSettings.equali_ai_content_capture_enabled and the tenant policy flag agents.telemetry.content_capture are true; the attached content is passed through redact_key_value first. Either flag off means no content in Logfire. Event payloads (gen_ai events) carry no prompt, completion or provider response body.
Data retention
Retention periods vary by provider and data type. Each sub-processor section above lists that provider's retention row; here is what those settings mean.
Provider-side retention is per sub-processor in (owner input). Platform-side, the composed prompt blocks live in Temporal workflow history for the history retention period and the persisted proposal/draft rows live under the tenant's normal data lifecycle and DSAR export. Prompt text is not stored in agent_runs, ai_invocations or the injection-screen records; only hashes, counts and costs are.
See the retention row in each provider's sub-processors section above for that provider's specific period.
Contact us
Questions about this page, your data, or how Lumtry uses AI? Reach either team below.
- Privacy
- privacy@agnotiq.com
- Legal
- legal@agnotiq.com