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
| Param | Type | Validation | Notes |
|---|---|---|---|
id | string (UUID) | ParseUUIDPipe | The challengeId returned by POST /auth/send-otp |
No body, no query.
Response
200 OK — ApiResponseOf<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"
}
}| Field | Type | Notes |
|---|---|---|
challengeId | string (UUID) | Echoed back |
purpose | OtpPurpose | One of: verify-phone-fan / verify-phone-profile / 2fa-setup / login-2fa |
phoneMask | string | null | Masked — last 4 digits visible (e.g., +90 ••• ••• ••12) |
expiresAt | string (ISO 8601) | Challenge expiry |
attemptsRemaining | number | auth.otp_max_attempts − Challenge.attempts |
resendAvailableAt | string (ISO 8601) | null | Earliest time another /resend-otp will succeed (cooldown gate) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
404 | auth.challenge.not_found | Challenge ID does not exist OR has expired |
429 | (throttle) | Rate limit exceeded (60 req/hour) |
Side effects
- Lookup
Challengebyid. - Mask phone for display (
maskPhone()helper — last 4 digits visible). - Compute
resendAvailableAtbased on last dispatch +auth.otp_resend_cooldown_seconds. - No mutations — pure read.
Code samples
curl https://api.bio.re/api/v1/auth/challenge/a1b2c3d4-e5f6-7890-abcd-ef1234567890type 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
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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/auth/auth.controller.ts | 383–401 (getChallenge) |
| DTO (response) | apps/api-core/src/modules/auth/dto/challenge.dto.ts | 27–44 (ChallengeMetadataResponseDto) |
| Service | apps/api-core/src/modules/auth/challenge.service.ts | getChallenge() |
| Prisma model | packages/prisma/prisma/schema.prisma | Challenge |