# Data provenance and PII

Where each field came from, why that matters before you automate on it, and how personal data is masked when a key lacks permission to read it.

Source: https://sourceloop.ai/help/api/provenance/

---

Two properties of this API decide whether an automation you build on it is safe. Both are unusual enough to be worth a page.

## Not every field came from the same place

A response mixes three kinds of field, and they carry different authority:

| Kind | Where it came from | Safe to act on |
|---|---|---|
| **Captured** | Observed by SourceLoop directly: the visit, the referrer, the campaign, the form submission, the touch sequence | Yes. This is our own first-hand record |
| **Mirrored** | Copied from a system you own, usually your CRM: deal stage, owner, company size, lifecycle status | With care. It is a copy, and it is only as fresh as `synced_at` |
| **Derived** | Computed by us from the two above: attributed revenue, cost per lead, model-weighted credit | Yes, but the number depends on the model and window you asked for |

Mirrored fields carry a `synced_at` timestamp. Check it before you write back. An automation that reads a stale mirror and pushes a "correction" into the system that actually owns the field will fight that system, and the loser is usually your data.

Fields you can write are documented as writable on the endpoint that exposes them. A deal value written through `PATCH /v1/deals/{id}` is applied to the CRM that owns the deal, not just to our copy, which is why it needs `deals:write` rather than a local scope.

## Absent is not zero

Where a number is unavailable, it comes back as `null`, not `0`. This is deliberate and it is the mistake most likely to poison an automated report.

"No ad account connected" is not "you spent nothing". "This contact has no deal" is not "this deal is worth zero". A dashboard that renders `null` as `0` will quietly show a cost per lead of zero and a founder will make a budget decision on it.

Every response also carries a `definitions` block explaining what each metric counts, so a figure from this API is never ambiguous about which day boundary or which model produced it.

## Personal data follows the key

Email addresses, phone numbers and names are returned masked unless the calling key carries `pii:read`:

```json
{ "email": "j•••@acme.com", "phone": null, "name": "J. D." }
```

Masking keeps the email domain, so a masked row still tells you the lead was at `acme.com`. The response says this has happened rather than leaving you to infer it, through `meta.pii_included` and a `pii_note`.

**New keys are created with `pii:read` included**, so this is a decision you make when you mint the key, not a protection you get for free. Keep it for integrations that genuinely need identity, such as syncing contacts into a CRM or sending a receipt. Remove it for everything else.

Everything else is a longer list than it first looks: reporting jobs, spreadsheet exports, BI syncs, and above all anything connected to an AI assistant. A key handed to [the MCP server](/help/mcp/) sends its responses to a third-party model provider, which may retain them, so that is the case where a key without `pii:read` is worth the small inconvenience.

Searching by email or phone counts as reading it, so those filters need the scope too.

Two consequences to design around:

- **Masked values are not stable identifiers.** Do not use them as a key to join or dedupe on. Use the contact id.
- **Turning the scope on changes the response**, not just what you can see. Test your parser against both shapes if the same code path serves keys of both kinds.

## Deleting data

Deletion is a support operation rather than an API call, because a delete has to span several stores that do not cascade, and a partial delete is worse than none. Contact us with the record ids and we will handle it as one operation.