Match Filter · Trust & Privacy

What happens to the NPI list you upload.

Match Filter lets you cross your own HCP target list against a Scope cohort. The hashing happens inside an AWS KMS-backed worker with per-tenant key derivation; the list is never stored, never sent to an EHR partner, and never visible to our engineers in either source or hashed form. This page walks through exactly how that works.

In plain English (read this if you're not a techie)

You upload a list of doctors you already care about. We use it once, only to count how many of them this cohort can reach, and then we let it go. The list never gets sent to any EHR partner - they don't see it and don't even know it exists. We don't save it on our side either. What you get back is a number (“X of your Y targets are reachable”) and a breakdown by specialty and region. That's it. No doctor names, no NPIs, nothing that says “this specific doctor matched.”

The two things people most worry about, answered:

  • Can the EHRs see who's on my list? No. They never receive it, are never notified, and aren't involved in this step at all.
  • Does Scriptlane keep my list? No. The file is processed in memory and discarded. We keep four numbers about it (counts + a scrambled fingerprint to recognize a re-upload) - never the contents.

The technical version (for security reviewers)

You upload a file of NPIs. The Next.js application authenticates the request and forwards the parsed NPIs to a dedicated AWS Lambda worker reachable only via a signed Function URL. The worker calls kms:GenerateMac with an HMAC_256 KMS key plus a per-tenant context string, deriving a tenant-scoped MAC key whose root never leaves KMS. Each NPI is HMAC'd with that derived key, the resulting tokens are intersected against the cohort the scoping fan-out produced, and a specialty + region rollup is returned. We persist only counts, the rollup, and a one-way list digest. NPIs - source or hashed - are never stored at rest, never logged, and never sent to an EHR partner. Every KMS derivation is recorded to CloudTrail; the application cannot read the HMAC root material under any circumstance.

What we persist, field by field

FieldValueWhy
uploaded_countintegerSo you can see how many rows we parsed.
matched_countintegerThe headline output - intersection size.
rejected_countintegerRows we couldn't parse or that failed Luhn; never the row contents.
result_summaryJSONThe same shape Scope uses - totals + by-specialty + by-region - restricted to matched HCPs. No per-HCP rows.
list_digestSHA-256 of sorted NPIs, per-org saltedLets us detect a re-upload of the same list. One-way; no NPI is recoverable from the digest.

What we never persist, process, or log

  • Your NPIs. Neither the full list nor the matched subset. The database schema doesn't have a column for them.
  • Per-HCP attribution. We never tell you which specific doctor matched which specific EHR partner. The rollup is aggregate.
  • EHR partner identity in the result. You see a specialty + region rollup of the matches; you don't see which EHR returned which match.
  • Your list in application logs. Our logger drops any field named npi, npis,list, or tokens before it writes, and our match code never passes NPI values to the logger directly.

How it travels and processes

1
Upload
Your file hits our endpoint over TLS 1.2+, authenticated against your Scope session. The multipart parser is in-memory; the file is never written to disk.
2
Validation
Every row is Luhn-checked against the NPI 80840 prefix. Any column that isn't npi is rejected. PHI-coded column names (mrn, patient_id, diagnosis, etc.) are rejected first with a specific reason so misuse is obvious.
3
Hashing (KMS-backed)
The parsed NPIs are forwarded to an isolated AWS Lambda worker over a signed Function URL. The worker calls kms:GenerateMac on an HMAC_256 KMS key with a per-tenant context string, deriving a tenant-scoped MAC key that never returns to the application. Each NPI is HMAC'd with that derived key. The HMAC root material never leaves KMS; every derivation is recorded to CloudTrail.
4
Intersection
The upload's token set is intersected with the token set of the Scope cohort the underlying fan-out produced. For each matched token we look up the specialty + state from the cohort's own records to build the aggregate rollup.
5
Persist + drop
We write the counts, rollup, and digest to match_filters. The parsed NPI array is reassigned to empty before the request returns. The Lambda worker terminates and its working memory is reclaimed; the application process never held the HMAC root material in the first place.
6
Audit
The upload, intersect, view, and discard events are written to match_audit_events with timestamps and counts - never NPIs. The customer-facing audit dashboard at /scope/app/audit shows every event for your org, with CSV export.

What this posture does and does not guarantee

The Match Filter worker is an isolated AWS Lambda whose only inbound surface is a signed Function URL the application calls per-request. It is the only component that can call kms:GenerateMac against the HMAC root key, and the application's IAM role cannot. That gives us the following:

  • The HMAC root never leaves KMS. Even a full compromise of the application or our Vercel environment cannot recover it. Per-tenant MAC keys are derived at request time and not persisted.
  • No engineer can read your list at rest. It isn't there. The DB schema has no column for uploaded NPIs or matched NPIs; both are dropped from memory before the request returns.
  • Every key derivation is independently logged. CloudTrail records every GenerateMac call - we cannot un-log a derivation, and you can request the CloudTrail extract for your tenant during an audit.

Honest disclosures, because this matters: the Lambda worker's working memory is not hardware-attested. A sufficiently-privileged AWS root operator with physical access to the host could in principle inspect the worker's memory while a request is in flight. AWS's own controls (service operator separation, no console access to running Lambdas, attested host hardware for the underlying compute) mitigate this; the Nitro Enclave path on the roadmap eliminates it for tenants who need cryptographic non-disclosure as a contract term.

Enterprise option

Enterprise customers can run Match Filter against a KMS customer-managed key in their own AWS account (BYOK). We receive the derived per-tenant MAC tokens for the intersection; you keep root control of the key and can revoke it on demand. Ask us if you need this.

What we reject on ingest

The deny-list runs before any processing. If your file has any of these columns, the upload fails with a specific error and no data is persisted:

  • PHI-coded: mrn, patient_id, patient_name, dob, ssn, diagnosis, icd10, dosage, medication, drug, pharmacy, insurance, payer, copay, and close variants.
  • Targeting-logic-coded: name, email, phone, specialty, state, zip, address - anything that lets a list-owner re-introduce filtering we explicitly keep out of this step.
  • Unknown: any column name we don't recognize. The allowlist is positive, not negative.

Questions we expect you to ask

Can I get the matched NPIs back as a list?
No. The output is aggregate only (counts + rollup). The matched NPIs are your own data - you uploaded them - so a download would just hand you back a subset of your original file, but emitting that would erode the posture we're asking you to trust. If you need a per-row list for an internal workflow, we can talk about an enterprise-tier flow that isolates that step behind additional controls.
Does the EHR partner see my list?
No. Your list never leaves Scriptlane. The EHR partner is not involved in the match step.
What if I re-upload the same list?
We detect it via the list digest and return the existing result instantly, without re-processing. The digest is per-org, so two different orgs uploading the same list don't collide.
What if I re-run the underlying scoping?
Re-running the scoping against a different brief produces a different network cohort, so you'll need to re-upload your list. We're building auto-rerun in v2.
Can a single-NPI list be uploaded to probe for presence?
On the Free plan, we enforce a 25-NPI minimum to mitigate this. Pro and Enterprise plans have no floor - paying customers have legitimate small-list use cases, and their usage is audit-logged.

How to report a concern

Email ben@scriptlanedata.com. Security issues get a one-business-day acknowledgement. If you have reason to believe the posture described on this page doesn't hold, tell us immediately and we'll publish a correction with the technical details of what changed.