BIO.RE
Authentication

Get Challenge Metadata

Fetch metadata for an active OTP Challenge (masked phone, expiry, remaining attempts). Does NOT return the code.

GET /api/v1/auth/challenge/{id} — 🌐 Public · Rate limit: 60 req / hour

Returns the metadata for an active Challenge so the UI can render appropriate state: masked phone (last 4 digits), expiry, attempts remaining, and the next resend availability time. Does NOT return the code — the code only travels via SMS.

This endpoint is public because Challenge ownership is implicit in the unguessable UUIDv4 id. The id itself acts as a capability token — if the client lost it (e.g., after a page refresh), they cannot re-acquire it without going through /send-otp again.

Request

Path parameters

ParamTypeValidationNotes
idstring (UUID)ParseUUIDPipeThe challengeId returned by POST /auth/send-otp

No body, no query.

Response

200 OKApiResponseOf<ChallengeMetadataResponseDto>

{
  "success": true,
  "data": {
    "challengeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "purpose": "verify-phone-fan",
    "phoneMask": "+90 ••• ••• ••12",
    "expiresAt": "2026-04-29T20:15:00.000Z",
    "attemptsRemaining": 4,
    "resendAvailableAt": "2026-04-29T20:01:30.000Z"
  }
}
FieldTypeNotes
challengeIdstring (UUID)Echoed back
purposeOtpPurposeOne of: verify-phone-fan / verify-phone-profile / 2fa-setup / login-2fa
phoneMaskstring | nullMasked — last 4 digits visible (e.g., +90 ••• ••• ••12)
expiresAtstring (ISO 8601)Challenge expiry
attemptsRemainingnumberauth.otp_max_attempts − Challenge.attempts
resendAvailableAtstring (ISO 8601) | nullEarliest time another /resend-otp will succeed (cooldown gate)

Errors

HTTPcode / i18nKeyReason
404auth.challenge.not_foundChallenge ID does not exist OR has expired
429(throttle)Rate limit exceeded (60 req/hour)

Side effects

  1. Lookup Challenge by id.
  2. Mask phone for display (maskPhone() helper — last 4 digits visible).
  3. Compute resendAvailableAt based on last dispatch + auth.otp_resend_cooldown_seconds.
  4. No mutations — pure read.

Code samples

curl https://api.bio.re/api/v1/auth/challenge/a1b2c3d4-e5f6-7890-abcd-ef1234567890
type ChallengeMetadata = {
  challengeId: string;
  purpose: 'verify-phone-fan' | 'verify-phone-profile' | '2fa-setup' | 'login-2fa';
  phoneMask: string | null;
  expiresAt: string;
  attemptsRemaining: number;
  resendAvailableAt: string | null;
};

async function getChallenge(challengeId: string): Promise<ChallengeMetadata> {
  const res = await fetch(`https://api.bio.re/api/v1/auth/challenge/${challengeId}`);
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery } from '@tanstack/react-query';

export const challengeKeys = {
  detail: (id: string) => ['auth', 'challenge', id] as const,
};

export function useChallenge(challengeId: string) {
  return useQuery({
    queryKey: challengeKeys.detail(challengeId),
    queryFn: async () => {
      const res = await fetch(`/api/v1/auth/challenge/${challengeId}`);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Failed'), {
          code: json?.error?.code,
        });
      }
      return json.data as ChallengeMetadata;
    },
    refetchInterval: 1000, // Tick the countdown UI every second
  });
}

Try it

GET
/api/v1/auth/challenge/{id}

Path Parameters

id*string

Response Body

application/json

application/json

curl -X GET "https://loading/api/v1/auth/challenge/string"
{
  "success": true,
  "data": {
    "challengeId": "007cfdcc-a46d-4340-a4c6-216ec2e4009c",
    "purpose": "verify-phone-fan",
    "phoneMask": "+90 ••• ••• ••12",
    "expiresAt": "2019-08-24T14:15:22Z",
    "attemptsRemaining": 5,
    "resendAvailableAt": "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"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/auth/auth.controller.ts383–401 (getChallenge)
DTO (response)apps/api-core/src/modules/auth/dto/challenge.dto.ts27–44 (ChallengeMetadataResponseDto)
Serviceapps/api-core/src/modules/auth/challenge.service.tsgetChallenge()
Prisma modelpackages/prisma/prisma/schema.prismaChallenge

On this page