Docs

#Sensitive data and PII

An approval needs enough in front of the reviewer to decide. That pulls toward copying the record into the decision, which is exactly what your PII controls exist to prevent.

The short version: link to the record, don't copy it. Put a URL to the Jira issue, the CRM record, the ticket or the invoice in the decision, and let the system that owns the data keep deciding who may open it. Where you need the approval bound to a specific person or address, put that value in facts, which is hashed before it leaves your process.

#What Ratifia stores

Two different things go into a request, and they are handled differently:

Where it goes
context (title, prompt, aiReasoning, data, blocks)Stored by Ratifia and shown to reviewers behind sign-in. This is the part to keep clean.
factsHashed to a SHA-256 factsHash in your own process. The values are never sent. See Approvals that can't be skipped.

A notification is a pointer, not a payload. By default the only piece of case content that leaves the boundary is the email headline (the decision's title), so keep PHI and PII out of titles specifically.

#Why linking is the better pattern

It isn't only about storing less. Three things about a decision are true today, and each one is handled for you by the system that already owns the record:

Ratifia does not record who read a decision. The audit trail records what happened to a decision: created, decided, refined, cancelled, expired, evidence attached, consumed. There is no "viewed" event. Your CRM and your issue tracker almost certainly do log reads, along with the access purpose your policies require. A link keeps that logging in the loop; a copy takes it out.

Who can read a decision depends on their role. An approver (decisions:read-mine) sees only the decisions they take part in: they are one of its approvers or its assignee, or they voted on it. A pending decision that names no approvers is visible to every approver, since any of them may answer it. Admins and anyone else holding decisions:read-all can open any decision in the org, including ones they were not asked to answer. Deciding is narrower still: when a decision names its approvers, only they may vote and anyone else is refused with 403 not_an_approver and recorded on the timeline. So embedding a record in context can widen who is able to see it beyond who could see it at the source. See Roles and permissions.

There is no per-record erasure. Deletion is a retention window, set per org in Settings > Data retention: off by default, and when on, between 7 and 3650 days. The sweeper hard-deletes whole decisions past the cutoff along with their notification records. Audit entries follow their own window. If you need to delete one subject's data on request, you need it to live somewhere with a delete: the source system.

Evidence cannot be unattached

Evidence is append-only. It can only be attached while a decision is PENDING, it is never edited or deleted, and the set is sealed once the decision resolves, so that what you see later is exactly what the approver could see. That is the point of it, and it is a reason to attach a link to a transcript rather than the transcript itself.

A decision usually touches more than one record: the ticket, the customer, the invoice. They go in a links block, which is the only way to put a link in a decision's context:

json
{  "context": {    "title": "Refund $1,250.00 on invoice INV-2201",    "prompt": "Customer reports a double charge. Refund it?",    "data": {      "amountCents": { "value": 125000, "src": "tool", "description": "Duplicate charge from Stripe" },      "customerRef": { "value": "cus_ACME", "src": "tool", "description": "Stripe customer id, not the customer's details" }    },    "blocks": [      { "kind": "links", "title": "Records", "links": [        { "label": "Invoice INV-2201", "url": "https://billing.acme.com/invoices/INV-2201" },        { "label": "Support ticket", "url": "https://acme.atlassian.net/browse/SUP-4471" },        { "label": "Customer", "url": "https://acme.my.salesforce.com/001x00000AbCdEf", "description": "Enterprise, renewed in March" }      ] }    ]  }}

The block renders as a labelled list the reviewer can click, in the dashboard and in a Slack or Discord notification carrying context. Your access controls decide the rest: if they can't open the record, they can't read it, and that's the point.

A link needs a label and an http(s) url; anything else is refused with 400, because these end up as links a reviewer clicks. At most 20 per block.

This is the only way to send a link. A URL sitting in a fields value stays plain text, and context.sourceUrl no longer exists: a request carrying it is refused, rather than quietly dropping the link. The one exception is the envelope to sign, which is docusign.documentUrl, because that is a thing to act on rather than a record to read.

Identifiers are usually enough for the reviewer to know which record is in play: cus_ACME, SUP-4471, the last four digits. Prefer them to names, addresses and account numbers, and let the link carry the identity.

#Binding the approval to a person, without sending the address

Linking answers what the reviewer sees. It does not answer the other question: how do you know the action that runs is the one that was approved, when the thing that matters is who it happens to?

That's what facts is for. Put the identifying value in facts and only its hash reaches Ratifia:

ts
import { hashFacts } from '@ratifia/sdk'
// One function builds the facts, used at the request and again at the action.// The recipient is in here; it never leaves as a value.const refundFacts = (r: Refund) => ({  recipient: r.customerEmail,  amountCents: r.amountCents,  invoice: r.invoiceId,})
// Requesting: send the hash. `context` carries the link, not the address.await fetch(`${apiUrl}/v1/worker/decisions`, {  method: 'POST',  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },  body: JSON.stringify({ ...base, factsHash: hashFacts(refundFacts(refund)), context }),})
// At the action: rebuild the facts from what you are actually about to do.await ratifia.consumeApproval({  decisionId,  facts: refundFacts(refund),  consumer: `refund:${refund.invoiceId}`,})

Change the recipient between the approval and the action and consumeApproval throws ApprovalNotValidError with facts_mismatch, so the refund never runs. The address is what makes that check meaningful, and Ratifia only ever held 64 hex characters.

The hash is one-way, so Ratifia cannot show the reviewer a value from it, and cannot search on it. That is the trade: the binding is strong, the display comes from context.

#When the value really has to be visible

Sometimes the reviewer genuinely cannot decide without seeing it, for example approving the recipient of an outbound email. Then:

  • Minimize first. A masked form (s••@customer.example, last four digits) is often enough to catch the error the review exists to catch.
  • Put the full value in facts as well, so the approval is bound to it even though the display is partial.
  • Set a retention window appropriate to what you have stored, rather than leaving it off.
  • Keep it out of the title, which is the one field that reaches email.

Rich notifications are an org-wide choice

Ratifia can embed a decision's context directly in notifications for teams that accept content leaving the boundary in exchange for at-a-glance review. It is enabled per org on request, never per caller, and any failure to resolve the setting falls back to the pointer form. If you have deliberately kept PII out of context, this setting is much safer to turn on.

#Questions we get

Does Ratifia sign a DPA / is it a processor for this data? For anything you put in context, yes, Ratifia is processing it on your behalf. Ask us at support@ratifia.com about current agreements and subprocessors. The reason this page recommends linking is that it shrinks that question rather than answering it.

Can I stop a specific reviewer from seeing a decision? Name its approvers. A reviewer with decisions:read-mine sees only the decisions they take part in, so one who is not an approver, the assignee or a voter on it cannot open it. Anyone holding decisions:read-all still can, so model sensitivity through what you put in context, and through who holds that permission.

Can I delete one decision? No. Deletion happens through the retention window, over whole decisions past a cutoff. Cancelling a decision withdraws it from the people who were asked; it does not erase it.

What does the model see? Nothing you do not send. Ratifia does not pass context to a model to generate approvals. Where an org has enabled LLM features, that is an explicit org setting and it is recorded in the audit trail (org_settings.llm_updated).