Update DM Config
Set DM type / price / active flag in one atomic call. Side effects sync DMPackage rows + flip manual social link visibility on DM ON↔OFF transitions. Public bio cache invalidated.
PATCH /api/v1/creators/:creatorId/dm-config — 🔑 Bearer · Rate limit: 30 req / hour
Atomic DM configuration write. Updates CreatorProfile.{dmType, dmPrice, dmActive}, syncs the active DMPackage row to match, and flips the visibility of manual social bio links on DM ON↔OFF transitions (because manual social links are forbidden while DM is active — see POST /creators/:creatorId/links).
Price range is admin-managed. Min / max are read from dm.min_price (default 1) and dm.max_price (default 500). The 5000 cap in the DTO @Max is a hard safety ceiling — actual enforced bounds come from ConfigService.
Social link side effects (transitional, not idempotent across runs):
- OFF → ON: every
BioLinkwithisSocial: true AND active: truein the creator's bio page is flipped toactive: false(so manual social links don't show alongside OAuth-verified ones while DM accepts paid messages). - ON → OFF: every
BioLinkwithisSocial: true AND active: falseis flipped back toactive: true.
These mirrors run inside the same transaction as the DM update. They do NOT discriminate "links the user themselves disabled" — if a creator manually deactivated a social link before turning DM on, it gets re-activated when DM turns off again.
Request
Path parameters
| Param | Type | Validation | Notes |
|---|---|---|---|
creatorId | string (UUID) | ParseUUIDPipe | Must match the bearer's CreatorProfile.id (otherwise 403) |
Body — DMConfigDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
dmType | enum | ✓ | IsEnum(DmType) | One of FREE / SINGLE_PAY / PER_MESSAGE |
dmPrice | number | conditional | IsNumber({ maxDecimalPlaces: 2 }), Min(0), Max(5000) | Required when dmType !== 'FREE' AND dmActive: true. Server enforces dm.min_price ≤ value ≤ dm.max_price (admin-managed). Stored as null when dmType === 'FREE'. |
dmActive | boolean | ✓ | IsBoolean() | Whether the creator currently accepts DMs |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — SuccessOnlyResponseDto
{
"success": true
}| Field | Type | Notes |
|---|---|---|
success | boolean | Always true on 200. The service computes deactivatedSocialLinks / reactivatedSocialLinks counts internally for telemetry, but the controller does not surface them in the response — re-fetch the creator profile via GET /creators/profile if you need the post-write social-link state. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | creator.dm.price_range | dmActive: true AND dmType !== 'FREE' AND dmPrice outside [dm.min_price, dm.max_price] (or missing). Message includes the current bounds. |
400 | (DTO validation) | Invalid dmType enum, dmPrice outside [0, 5000] hard ceiling, missing fields |
401 | (guard) | Missing / invalid bearer token |
403 | (verifyCreatorOwnership) | creatorId does not belong to the bearer's user |
429 | (throttle) | Rate limit exceeded (30 req/hour) |
Side effects
- Ownership check —
verifyCreatorOwnership(creatorId, userId)→ 403 on mismatch. - Price gate — when
dmType !== 'FREE'ANDdmActive: true: readdm.min_price/dm.max_price(admin defaults 1 / 500); reject ifdmPricemissing or outside range. - Read current
creator.dmActive(for transition detection:wasActivevswillBeActive). - Inside one transaction:
creatorProfile.update({ dmType, dmPrice: dmType === 'FREE' ? null : dmPrice, dmActive }).- DMPackage sync:
- When
dmType !== 'FREE' AND dmActive: find the active package; same type → update price; different type → deactivate old + create new; none → create new. - When
FREEordmActive: false: deactivate every active package (updateMany active=false).
- When
- Social link transition (only fires on flip, no-op if
wasActive === willBeActive):- OFF→ON:
bioLink.updateMany({ where: bioPageId AND isSocial AND active, data: { active: false } }). Count →deactivatedSocialLinks. - ON→OFF:
bioLink.updateMany({ where: bioPageId AND isSocial AND NOT active, data: { active: true } }). Count →reactivatedSocialLinks.
- OFF→ON:
- Audit log:
[dm] Config set for creator {id}: {dmType} active={dmActive} (deactivated=N, reactivated=N). - Cache invalidation —
invalidateBioCache(creatorId).
Code samples
curl -X PATCH https://api.bio.re/api/v1/creators/c1a2b3c4-d5e6-7890-abcd-ef1234567890/dm-config \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"dmType": "SINGLE_PAY",
"dmPrice": 5,
"dmActive": true
}'type SetDMConfigInput = {
dmType: 'FREE' | 'SINGLE_PAY' | 'PER_MESSAGE';
dmPrice?: number; // required when dmType !== 'FREE' && dmActive
dmActive: boolean;
};
async function setDMConfig(accessToken: string, creatorId: string, input: SetDMConfigInput): Promise<void> {
const res = await fetch(`https://api.bio.re/api/v1/creators/${creatorId}/dm-config`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Update DM config failed'), {
code: json?.error?.code,
});
}
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useSetDMConfig(creatorId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: SetDMConfigInput) => {
const res = await fetch(`/api/v1/creators/${creatorId}/dm-config`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Update DM config failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['creators', creatorId, 'dm-config'] });
// Bio links may have been toggled — refetch the editor view too
qc.invalidateQueries({ queryKey: ['creators', creatorId, 'bio'] });
qc.invalidateQueries({ queryKey: ['creators', 'profile'] });
},
});
}Try it
Authorization
bearer In: header
Path Parameters
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
curl -X PATCH "https://loading/api/v1/creators/string/dm-config" \ -H "Content-Type: application/json" \ -d '{ "dmType": "FREE", "dmActive": true }'{
"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"
}
}{
"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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/creator/creator.controller.ts | 213–224 (setDMConfig) |
| DTO (request) | apps/api-core/src/modules/creator/dto/creator.dto.ts | 117–126 (DMConfigDto) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 489–563 (setDMConfig) |
| Config keys | apps/api-core/src/modules/config/config.service.ts | dm.min_price (default 1), dm.max_price (default 500) — admin-managed |
| Prisma models | packages/prisma/prisma/schema.prisma | CreatorProfile.dm*, DMPackage, BioLink.isSocial, BioLink.active |