BIO.RE
Notifications

Update Notification Preference

Save the user's per-channel override for a single event. Upserts a NotificationPreference row. eventKey must exist in the admin-managed defaults catalog.

PATCH /api/v1/notifications/preferences/:eventKey โ€” ๐Ÿ”‘ Bearer ยท Rate limit: 30 req / minute

Upserts the user's NotificationPreference row for a single eventKey. Only the channel booleans you supply are touched (sparse update); on first write, missing channels default to true. The server validates that eventKey exists in the admin-managed NotificationDefault catalog (else 404 event_not_found).

true is the create default. When this is the user's first save for an eventKey (no override row yet), every channel you don't explicitly send defaults to true. To opt-out of every channel except email, you must explicitly send push: false, inApp: false, webPush: false, sms: false โ€” sending only email: true would leave the others enabled.

Subsequent updates are sparse. Once an override row exists, only the channels you send are written. Omitted channels keep their stored values. The asymmetry between "first save = true defaults" and "subsequent save = preserve" is a Prisma upsert consequence โ€” the update branch only touches supplied keys, the create branch fills the rest.

Request

Path parameters

ParamTypeValidationNotes
eventKeystring(no UUID pipe โ€” raw string)Must match a row in NotificationDefault (server checks). E.g. new_message_creator, payout_processed.

Body

A loose object โ€” server uses class-validator-free pass-through, but the schema documents 5 boolean channel fields.

FieldTypeRequiredNotes
emailbooleanoptionalEmail channel toggle
pushbooleanoptionalNative push toggle
inAppbooleanoptionalIn-app feed toggle
webPushbooleanoptionalWeb push toggle
smsbooleanoptionalSMS toggle
HeaderRequiredNotes
Authorization: Bearer <accessToken>โœ“JWT from POST /auth/login

Response

200 OK โ€” SuccessOnlyResponseDto

{
  "success": true
}
FieldTypeNotes
successbooleanAlways true on 200. Re-fetch GET /notifications/preferences to read the merged post-write state (the row will now have customized: true).

Errors

HTTPcode / i18nKeyReason
401(guard)Missing / invalid bearer token
404error.notification.event_not_foundeventKey doesn't match any NotificationDefault row (catalog gate)
429(throttle)Rate limit exceeded (30 req/min โ€” tighter than other endpoints)

Side effects

  1. Catalog gate โ€” notificationDefault.findUnique({ where: { eventKey } }). Missing โ†’ throw event_not_found.
  2. Upsert โ€” notificationPreference.upsert({ where: { userId_eventKey }, update: <body>, create: <body with true defaults for unset channels> }).
  3. Return { success: true }. No notification dispatch effects โ€” preference takes effect on the NEXT event fired.

Code samples

# Disable email for new-message-creator events
curl -X PATCH https://api.bio.re/api/v1/notifications/preferences/new_message_creator \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"email": false}'

# Opt-out of every channel except in-app (first save โ€” must be explicit)
curl -X PATCH https://api.bio.re/api/v1/notifications/preferences/payout_processed \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"email": false, "push": false, "webPush": false, "sms": false, "inApp": true}'
type ChannelToggles = {
  email?: boolean;
  push?: boolean;
  inApp?: boolean;
  webPush?: boolean;
  sms?: boolean;
};

async function updateNotificationPreference(
  accessToken: string,
  eventKey: string,
  channels: ChannelToggles,
): Promise<void> {
  const res = await fetch(`https://api.bio.re/api/v1/notifications/preferences/${eventKey}`, {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(channels),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Preference update failed'), {
      code: json?.error?.code,
    });
  }
}
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useUpdateNotificationPreference() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (vars: { eventKey: string; channels: ChannelToggles }) => {
      const res = await fetch(`/api/v1/notifications/preferences/${vars.eventKey}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(vars.channels),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Preference update failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['notifications', 'preferences'] });
    },
  });
}

Try it

PATCH
/api/v1/notifications/preferences/{eventKey}
AuthorizationBearer <token>

In: header

Path Parameters

eventKey*string

Response Body

application/json

application/json

application/json

curl -X PATCH "https://loading/api/v1/notifications/preferences/string"
{
  "success": true
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controller (inline impl)apps/api-core/src/modules/notification/user-notification.controller.ts151โ€“181 (updatePreference โ€” catalog gate + Prisma upsert)
DTO (response)apps/api-core/src/common/dto/common-response.dto.tsSuccessOnlyResponseDto
Prisma modelspackages/prisma/prisma/schema.prismaNotificationDefault (catalog gate), NotificationPreference (compound unique userId_eventKey)

On this page