Security & data masking
Authentication, sensitive-data redaction, and ingestion best practices.
ChurnWarn is built to do its job — score churn risk — without becoming a second copy of your customers' PII. This page covers how to authenticate safely and how the SDKs keep sensitive data out of your event stream.
Authentication
| Method | Header | Where |
|---|---|---|
Website key (cw_pub_…) | X-Api-Key: <key> | Browser. Ingest only: POST /api/events, PUT /api/accounts. |
Server key (cw_sec_…) | X-Api-Key: <key> | Server-side only. Full tenant access. |
| Bearer JWT | Authorization: Bearer <jwt> | Dashboard sessions. Tenant from tenant_id claim. |
A cw_sec_ key can read every account, score and setting in your
project. Client code gets a website key instead: the gateway
rejects it (403) on any endpoint other than ingest, so a visitor who reads it
out of your page source can only send events.
- Rotate keys from Settings → API keys; revoke any key that may have leaked. A key is shown in full only once, at creation — afterwards only its prefix.
- CORS: the ingest endpoints accept any origin and carry no credentials, so the browser SDK works from any customer domain with no allow-list entry. Every other endpoint stays restricted to the dashboard's own origins.
- Tenant scoping: omit
tenantIdto use the credential's tenant. Only pass it to target another tenant you're explicitly authorized for.
Sensitive-data masking
Every SDK redacts payloads in-process, before an event is queued — unmasked
data never leaves your application. Masking is on by default, bounded, and never
throws (it falls back to a safe ***).
redact_payload / redactPayload). Sensitive values are masked in-process before an event is queued, so unmasked data never leaves your application. Masking is bounded, never throws, and falls back to a safe *** on any error.Values detected and masked inside strings
| Pattern | Becomes |
|---|---|
| Email addresses | j***e@example.com (first/last char kept) |
| Credit-card numbers | ****-****-****-1234 (last 4 kept) |
| Phone numbers | 415****6789 |
| US SSN-like values | ***-**-**** |
| JWTs (eyJ…header.payload.sig) | eyJhbGciOi...*** |
| API keys / bearer tokens (key: value) | abcd*** (first 4 kept) |
| Passwords embedded in URLs | user:********@host |
| IBANs | DE89***6789 |
Keys always replaced with ***
passwordsecrettokenapi_keyauthorizationcookiessncredit_cardAny key containing one of these words has its value redacted. Keys named url / referrer (or ending in url) keep only their path — query strings and hash fragments are stripped.
- Send
pathorrouteinstead of full URLs in server-side payloads. - Never use an email or username as
external_account_id— use a stable non-PII id. - Masking is a safety net, not a license to send secrets. Keep payloads minimal.
What ChurnWarn intentionally does not collect
- The browser SDK sends only
origin + pathnameforurl/referrer— never query strings or hash fragments. element_clickednever captures visible button/link text; labels come from stable attributes and are themselves masked.- There is no automatic form-field value capture.
Recommendations
- Use a stable, non-PII value for
externalAccountId(an internal id, not an email). It appears throughout the product UI. - Keep payloads minimal — send the few fields a metric needs, not whole objects. Redaction is a safety net, not a reason to send secrets.
- Prefer
path/routeover full URLs in server-side payloads. - Use
onBeforeEnqueue/OnBeforeEnqueueto drop or transform fields you never want to leave your boundary. - Keep a stable
idempotencyKeyper logical event so retries can't double-count.
Masking protects against accidental leakage of common patterns. It does not replace your own review of what each event carries. Treat the payload schema as part of your data-handling surface and keep it lean.