Submit Appeal
File an appeal against a moderation action (ban, fraud flag, content removal, report action). Reachable by banned users via BypassBanCheck. One pending appeal per user at a time.
POST /api/v1/users/appeals โ ๐ Bearer ยท Rate limit: 3 req / day
Submits an appeal against a moderation action. Calls into the trust-safety module (not the user service) to create an Appeal row with status = PENDING. The endpoint is decorated @BypassBanCheck() so banned / suspended users can reach it โ that's the whole reason it exists on the user-facing path.
@BypassBanCheck is a deliberate exception. Most user-facing endpoints reject banned/suspended accounts via BanCheckGuard. This endpoint is whitelisted because the platform must give blocked users a way to appeal โ refusing that would be procedurally unfair.
Only one pending appeal at a time. If an Appeal already exists with status IN (PENDING, UNDER_REVIEW), the call is rejected with 400 error.trust_safety.appeal_already_pending. Wait for moderation to resolve the open one (or hit the 3/day limit) before filing another.
Request
Body โ SubmitAppealDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
type | string | โ | IsIn(['ban','fraud_flag','content_removal','report_action']) | Which moderation decision is being contested |
reason | string | โ | MaxLength(5000) | Free-form explanation โ surface to the moderator review queue |
banId | string (UUID) | optional | IsUUID() | The Ban row being appealed (recommended when type = 'ban') |
evidence | string | optional | MaxLength(5000) | Supporting evidence / additional context |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | โ | JWT from POST /auth/login. The endpoint accepts the JWT even if the account is BANNED / SUSPENDED. |
Response
201 Created โ ApiResponseOf<AppealSubmittedDto>
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "PENDING",
"createdAt": "2026-04-29T20:00:00.000Z"
}
}| Field | Type | Notes |
|---|---|---|
id | string (UUID) | Appeal.id |
status | enum | One of PENDING / REVIEWING / RESOLVED / DISMISSED. Always PENDING immediately after creation. |
createdAt | string (ISO 8601) | Server-side timestamp |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | Invalid type enum, fields exceeding 5000 chars, malformed banId UUID |
400 | error.trust_safety.appeal_already_pending | An Appeal with status IN (PENDING, UNDER_REVIEW) already exists for this user |
401 | (guard) | Missing / invalid bearer token |
429 | (throttle) | Rate limit exceeded (3 req/day) |
Side effects
- No ban-check โ
@BypassBanCheckskips the gate that normally blocks banned/suspended users. - Look for existing
Appeal { userId, status IN (PENDING, UNDER_REVIEW) }. If found โappeal_already_pending. prisma.appeal.create({ data: { id: randomUUID(), userId, type, reason, banId: banId ?? null, evidence: evidence ?? null } }).- Surfaces in the moderation review queue โ operator response is out-of-band (notification when status flips).
Code samples
curl -X POST https://api.bio.re/api/v1/users/appeals \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"type": "ban",
"reason": "I believe my ban was issued in error.",
"banId": "b1a2n3i4-d5f6-7890-abcd-ef1234567890",
"evidence": "Logs from the disputed session."
}'type AppealType = 'ban' | 'fraud_flag' | 'content_removal' | 'report_action';
type SubmitAppealInput = {
type: AppealType;
reason: string;
banId?: string;
evidence?: string;
};
type AppealSubmitted = {
id: string;
status: 'PENDING' | 'REVIEWING' | 'RESOLVED' | 'DISMISSED';
createdAt: string;
};
async function submitAppeal(accessToken: string, input: SubmitAppealInput): Promise<AppealSubmitted> {
const res = await fetch('https://api.bio.re/api/v1/users/appeals', {
method: 'POST',
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 ?? 'Appeal submission failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation } from '@tanstack/react-query';
export function useSubmitAppeal() {
return useMutation({
mutationFn: async (input: SubmitAppealInput) => {
const res = await fetch('/api/v1/users/appeals', {
method: 'POST',
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 ?? 'Appeal submission failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as AppealSubmitted;
},
});
}Try it
Authorization
bearer In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/users/appeals" \ -H "Content-Type: application/json" \ -d '{ "type": "ban", "reason": "I believe my ban was issued in error." }'{
"success": true,
"data": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"status": "PENDING",
"createdAt": "2019-08-24T14:15:22Z"
}
}{
"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/user/user.controller.ts | 273โ282 (submitAppeal) |
| DTO (request) | apps/api-core/src/modules/trust-safety/dto/submit-appeal.dto.ts | 4โ17 (SubmitAppealDto) |
| DTO (response) | apps/api-core/src/modules/user/dto/user-client-response.dto.ts | 274โ283 (AppealSubmittedDto) |
| Service | apps/api-core/src/modules/trust-safety/trust-safety.service.ts | 1282โ1303 (submitAppeal) |
| Decorator | apps/api-core/src/common/decorators/bypass-ban-check.decorator.ts | BypassBanCheck() (skips BanCheckGuard) |
| Prisma models | packages/prisma/prisma/schema.prisma | Appeal, AppealStatus, Ban |
List Blocked Users
Read the caller's block list. Returns up to 50 most recently blocked users plus the total count. Pagination is not exposed by the controller (fixed at page 1, limit 50).
Get Attribution
Read the immutable registration attribution snapshot โ UTM params, first referrer, landing page, device, country, referral chain. Captured once at signup and never changes.