Privacy & LGPD
Your contacts are your business's most valuable asset. bZapper is built to protect that base — not only because Brazil's LGPD (Law 13.709/2018) requires it, but because a banned number and a burned list are expensive. This page describes the privacy controls that already ship in the product and how to use them.
bZapper is the data processor: it provides the consent, suppression and security tools. You (the controller) decide the purposes and guarantee the legal basis for each contact. Both sides work together — the product automates the heavy lifting; the legal basis for consent is yours.
Legal basis for consent
The LGPD requires a legal basis to process personal data. For marketing and notifications over WhatsApp, the most common basis is consent — which must be:
- Free — no coercion; the person can say no without penalty.
- Informed — they know who you are and why they'll receive messages.
- Unambiguous — a clear "yes" (an active opt-in, not a pre-ticked box).
Other bases in the law also appear in the product: contract performance (contract)
and legitimate interest (legitimate_interest) — useful for transactional messages
(OTP, order confirmation, billing).
Consent ledger (consent_ledger)
Every time a contact opts in or opts out, bZapper can record the event in an
append-only ledger — the consent_ledger. It's a timestamped, immutable proof that
consent was (or ceased to be) given, exactly what the LGPD asks for when you need to
demonstrate the legal basis.
Each record keeps:
| Field | What it is |
|---|---|
phone | normalized phone (digits only, with country code) |
channel | consent channel (default whatsapp) |
basis | legal basis: consent, contract or legitimate_interest |
granted | true = opt-in, false = recorded opt-out |
source | proof origin: form, import, api, keyword:parar… |
evidence | proof text/URL (e.g. link to the signed form) |
created_at | date and time of the event |
The ledger only takes inserts — it is never edited or deleted row by row. That's what makes it auditable: a contact's consent history is a chronological sequence of facts, not a state you can rewrite.
Keyword opt-out
Anyone who replies STOP, SAIR, PARAR (among others) to a message is automatically added to the project's suppression list — and never receives another send. bZapper detects the opt-out request on inbound, with no code from you.
Recognized words (case-insensitive, accent- and punctuation-tolerant):
parar · pare · sair · cancelar · descadastrar · remover · chega
stop · unsubscribe · "sem promoção" · "não quero" · "não perturbe" · "me tira"
Detection is conservative: it matches the whole short message or the first word —
"I can't stop praising you" does not suppress anyone. When an opt-out is recorded
for the first time, bZapper emits the contact.opted_out webhook.
There is no "start/back-in" keyword on inbound — for safety, re-subscription is always manual (an admin removes the contact from suppression). This prevents an accidental "start" from reopening the channel for someone who asked to leave.
Two-level block list
Suppression protects your number (and its reputation) by blocking the send before the message hits the queue. There are two levels:
- Wall (always blocks) —
opt_out(the person asked to leave) andmanual(an admin blocked). These never get through, not even withforce. - Inferred (strict mode) —
blocked_inferred(messages stuck at "sent" that never reached "delivered") andhard_bounce. These only block if the account enables strict mode (enforce_unreachable_suppression); otherwise they're just a signal.
Each record keeps the reason (reason) and the source (source, e.g.
keyword:parar, campaign:<id>, admin), so you always know why a number was
suppressed. The check applies to single sends and campaigns — it lives in the shared
outbound path.
Private media with presign
All conversation media (images, audio, documents arriving via WhatsApp) is private:
- Stored in a private bucket — never in a public directory.
- Downloaded via a pre-signed URL with a short lifetime (TTL). The default is 24
hours (
MEDIA_URL_TTL), and the link is regenerated on each access in the inbox. - The client downloads straight from the bucket — the bytes don't pass through your API.
- Signed URLs use HMAC-SHA256 and are time-validated: an expired link won't open.
In short: a conversation file is never left "loose" on a public, indexable URL. Without a signed and still-valid URL, no one can access it.
Project isolation
Within your account, each project is an isolated environment (see Account, projects, and users). Numbers, inbox, keys, statistics and the suppression list are per project. An authenticated call only sees the project its key/session belongs to:
- API key → carries the project embedded; every operation is scoped to it.
- Panel session → the active project goes in the
X-Project-Idheader, validated against your account on every request.
In the database, queries are always filtered by tenant_id and project_id — a project
never reads another's data.
Retention and deletion
Conversation data doesn't stay forever. bZapper runs a retention routine that, per your plan, deletes old media and messages:
- Expired media has its bytes deleted from the bucket (frees quota), keeping the message row.
- Expired messages are deleted; the attached media is purged first (a guarantee that nothing is orphaned in the bucket).
Manual deletion uses the same path — deleting a message deletes its media too.
Other security controls
- Signed webhooks — each delivery carries
X-Bzapper-Signature(HMAC-SHA256) for you to verify authenticity; the comparison is timing-safe. See Webhooks. The webhook secret is encrypted at rest. - Encrypted secrets — WhatsApp sessions are encrypted at rest; the OTP code is never persisted nor displayed.
- Rate limiting — each API key has a per-second request limit (token-bucket).
- Normalized phone and email — phone becomes digits-only with country code; email is lowercased in the database — the same contact is recognized without duplicates.
Checklist to start right
- Collect consent freely, informed and unambiguously — and keep the proof.
- Record the legal basis of each contact (the ledger exists for this).
- Let opt-out flow — don't block the keywords; they protect you.
- Listen to
contact.opted_outto sync with your CRM. - Respect suppression — don't try to re-import anyone who asked to leave.
bZapper handles opt-out, suppression and the consent ledger automatically, but you are the one who guarantees each contact authorized receiving your messages. Only message people who opted in.