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.
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:
{ "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 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.