Skip to main content

Subscriber preferences

Honouring an opt-out is a legal requirement under the Nigeria Data Protection Act (NDPA 2023) and GDPR. In Nervly, subscriber preferences are stored in PostgreSQL and evaluated by delivery workers immediately before dispatch, ensuring suppressed channels are never contacted and never billed. The controller–processor terms and the rest of the privacy posture are set out in the privacy policy.

Workspace-scoped preferences​

Subscriber preferences are strictly isolated per tenant workspace (UNIQUE(workspace_id, subscriber_id)). Preferences configured in one workspace never collide with another workspace.

Updating channel and category preferences​

PUT /v1/users/{subscriberId}/preferences is how you honour opt-outs. The write is a merge, not a replace, one level deep: a channel you omit keeps its current value, and a category you send replaces that category's flags wholesale. To re-enable a channel, send it explicitly as true. The merge happens inside PostgreSQL rather than as a read-modify-write, so two concurrent updates to different channels cannot lose each other.

The request body is a UserPreferencesRequest:

  • channels — per-channel booleans. All five delivery channels are accepted: email, push, sms, voice, and whatsapp. Omitting a channel leaves it untouched; setting it to false suppresses every notification on that channel for the subscriber, including ones a workflow marks CRITICAL.
  • categories — your own category name mapped to a channel-boolean object, for example {"marketing": {"email": false, "sms": false}}. Category rules take precedence over the channel flags.

Supplying neither channels nor categories is a 400 BAD_REQUEST; there is nothing to write.

curl --fail-with-body -sS -X PUT https://api.nervly.io/v1/users/user_8f21c/preferences \
--connect-timeout 5 \
--max-time 10 \
-H "Authorization: Bearer $NERVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channels": {
"email": true,
"push": false,
"sms": true,
"voice": false,
"whatsapp": false
},
"categories": {
"marketing": { "email": false, "sms": false },
"security": { "email": true, "sms": true }
}
}'

On success the gateway returns 200 with a UserPreferencesResponse:

{
"status": "UPDATED",
"subscriberId": "user_8f21c",
"updated_at": "2026-09-11T20:15:00.123Z"
}

Reading preferences back​

There is no public GET for preferences on the gateway yet. The PUT response above is the confirmation that the stored state changed; the gateway does not expose a read endpoint to fetch it. The subject-access export described under Subject access export is the supported way to retrieve a subscriber's full record and preference history, and it runs on the console host rather than the API gateway. Do not build against GET /v1/users/{subscriberId}/preferences: that path accepts only PUT, so a GET is not part of the contract and is rejected.

Errors​

Every non-2xx response uses the same flat three-field ErrorResponse:

{
"error": "BAD_REQUEST",
"message": "Supply at least one of channels or categories",
"status_code": 400
}

PUT /v1/users/{subscriberId}/preferences can return:

StatuserrorCauseRetry?
400BAD_REQUESTNeither channels nor categories was suppliedNo — fix the request
401UNAUTHORIZEDMissing or invalid API keyNo — fix the key
500INTERNAL_SERVER_ERRORThe preference store is unavailable and nothing was writtenYes

Each example above branches on the HTTP status and extracts error and message from the body. Branch on error, never on message. See Errors for the full envelope and retry policy.

Subscriber erasure (NDPR Right to Erasure)​

DELETE /v1/subscribers/{subscriberId} erases a subscriber to satisfy NDPA 2023 data-subject erasure mandates. It is idempotent: erasing a subscriber that does not exist (or erasing the same subscriber twice) still returns 202, so there is no 404 to handle. The response is a SubscriberErasureResponse:

curl --fail-with-body -sS -X DELETE https://api.nervly.io/v1/subscribers/user_8f21c \
--connect-timeout 5 \
--max-time 10 \
-H "Authorization: Bearer $NERVLY_API_KEY"

The 202 Accepted response confirms the erasure:

{
"status": "accepted",
"subscriberId": "user_8f21c",
"message": "Subscriber erasure initiated and completed"
}

Erasure can return 401 UNAUTHORIZED for a missing or invalid key and 500 INTERNAL_SERVER_ERROR when the preference store is unavailable and the erasure failed. It does not return 404; see Errors for the flat error envelope.

What happens on erasure:

  1. The subscriber record is anonymised: email, phone, and push tokens are cleared and the profile is marked deleted.
  2. Stored channel preferences for the subscriber are removed.
  3. Contact fields on messages older than 30 days are nulled.
  4. If a message is currently enqueued for the erased subscriber, the worker detects the deletion and suppresses delivery with status SUPPRESSED.
  5. An immutable entry is written to the tamper-evident audit trail.

Nigerian NCC 2442 DND compliance​

For SMS traffic terminating in Nigeria (+234), Nervly integrates mandatory Do-Not-Disturb (DND 2442) compliance directly into the Go worker router:

  1. MSISDN Normalization: Automatically standardizes Nigerian phone numbers (+234..., 080..., 090..., 070..., 081...) to international E.164 digits without + for provider lookup.
  2. DND Cache: DND lookups are cached for 1 hour, keyed by a non-reversible SHA-256 digest of the number — no plaintext phone number is ever stored in the cache.
  3. Asymmetric Failure Posture:
    • Marketing Traffic: Fails closed. If DND status check fails or indicates DND is active, the SMS is suppressed (SUPPRESSED).
    • Transactional / OTP Traffic: Fails open. Critical security and transactional OTPs route via high-priority transactional routes (corporate CLI) rather than failing.
  4. Audit Evidence: Every DND decision is recorded to an immutable, tamper-evident audit trail with a SHA-256 hashed MSISDN, intent, category, and outcome — preserving verifiable regulatory evidence without storing raw recipient numbers.

Subject access export​

To satisfy NDPA 2023 data access requests, administrators can export a subscriber's complete history as JSON from the console:

curl https://console.nervly.io/console/subscribers/user_8f21c/export \
-H "Cookie: nervly_session=$SESSION_COOKIE"

This is the retrieval path for a subscriber's stored preferences and message history: the public gateway exposes no GET for preferences. The export runs on the console host and is authenticated with an administrator session rather than an API key.

Next​