BIO.RE
User

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

FieldTypeRequiredValidationNotes
typestringโœ“IsIn(['ban','fraud_flag','content_removal','report_action'])Which moderation decision is being contested
reasonstringโœ“MaxLength(5000)Free-form explanation โ€” surface to the moderator review queue
banIdstring (UUID)optionalIsUUID()The Ban row being appealed (recommended when type = 'ban')
evidencestringoptionalMaxLength(5000)Supporting evidence / additional context
HeaderRequiredNotes
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"
  }
}
FieldTypeNotes
idstring (UUID)Appeal.id
statusenumOne of PENDING / REVIEWING / RESOLVED / DISMISSED. Always PENDING immediately after creation.
createdAtstring (ISO 8601)Server-side timestamp

Errors

HTTPcode / i18nKeyReason
400(DTO validation)Invalid type enum, fields exceeding 5000 chars, malformed banId UUID
400error.trust_safety.appeal_already_pendingAn 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

  1. No ban-check โ€” @BypassBanCheck skips the gate that normally blocks banned/suspended users.
  2. Look for existing Appeal { userId, status IN (PENDING, UNDER_REVIEW) }. If found โ†’ appeal_already_pending.
  3. prisma.appeal.create({ data: { id: randomUUID(), userId, type, reason, banId: banId ?? null, evidence: evidence ?? null } }).
  4. 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

POST
/api/v1/users/appeals
AuthorizationBearer <token>

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

SourcePathLines
Controllerapps/api-core/src/modules/user/user.controller.ts273โ€“282 (submitAppeal)
DTO (request)apps/api-core/src/modules/trust-safety/dto/submit-appeal.dto.ts4โ€“17 (SubmitAppealDto)
DTO (response)apps/api-core/src/modules/user/dto/user-client-response.dto.ts274โ€“283 (AppealSubmittedDto)
Serviceapps/api-core/src/modules/trust-safety/trust-safety.service.ts1282โ€“1303 (submitAppeal)
Decoratorapps/api-core/src/common/decorators/bypass-ban-check.decorator.tsBypassBanCheck() (skips BanCheckGuard)
Prisma modelspackages/prisma/prisma/schema.prismaAppeal, AppealStatus, Ban

On this page