Authentication
Resend Phone OTP
Generate a fresh OTP code, reset attempts, dispatch a new SMS. Caps per challenge + cooldown between resends.
POST /api/v1/auth/resend-otp — 🌐 Public · Rate limit: 10 req / hour
Generates a fresh code for an existing Challenge, resets attempts = 0, increments resendCount, and dispatches a new SMS via the active SMS provider (admin-managed). Two server-side caps:
auth.otp_max_resends— per-challenge resend ceiling.auth.otp_resend_cooldown_seconds— minimum seconds between resends.
Request
Body — ResendOtpDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
challengeId | string (UUID) | ✓ | IsUUID() | Returned by POST /auth/send-otp |
Response
200 OK — ApiResponseOf<ChallengeDispatchResponseDto>
{
"success": true,
"data": {
"challengeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"expiresAt": "2026-04-29T20:25:00.000Z",
"attemptsRemaining": 5,
"resendCount": 1
}
}Same shape as /send-otp but with resendCount populated.
| Field | Type | Notes |
|---|---|---|
challengeId | string (UUID) | Same id — Challenge row reused, code rotated |
expiresAt | string (ISO 8601) | New TTL window from now |
attemptsRemaining | number | Reset to auth.otp_max_attempts |
resendCount | number | Total dispatches so far for this Challenge |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | auth.otp.resend.cap_reached | Challenge.resendCount >= auth.otp_max_resends |
400 | auth.otp.resend.cooldown | Last dispatch within auth.otp_resend_cooldown_seconds |
404 | auth.otp.resend.not_found | challengeId does not exist |
429 | (throttle) | Rate limit exceeded (10 req/hour) |
Side effects
- Lookup
ChallengebychallengeId; verify exists, not used, not expired. - Cap check:
Challenge.resendCount < auth.otp_max_resendsAND last dispatch ≥auth.otp_resend_cooldown_secondsago. - Generate fresh 6-digit code; bcrypt-hash; update
Challenge.codeHash. - Reset
Challenge.attempts = 0; incrementChallenge.resendCount; refreshexpiresAt. - Dispatch SMS via the active SMS provider (admin-managed via
external.sms.active_provider). - Audit log:
auth.otp.resend.success(with masked phone).
Code samples
curl -X POST https://api.bio.re/api/v1/auth/resend-otp \
-H 'Content-Type: application/json' \
-d '{"challengeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}'async function resendOtp(challengeId: string): Promise<ChallengeDispatch> {
const res = await fetch('https://api.bio.re/api/v1/auth/resend-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ challengeId }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Resend OTP failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation } from '@tanstack/react-query';
export function useResendOtp() {
return useMutation({
mutationFn: async (challengeId: string) => {
const res = await fetch('/api/v1/auth/resend-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ challengeId }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Resend OTP failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as ChallengeDispatch;
},
});
}Try it
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 POST "https://loading/api/v1/auth/resend-otp" \ -H "Content-Type: application/json" \ -d '{ "challengeId": "007cfdcc-a46d-4340-a4c6-216ec2e4009c" }'{
"success": true,
"data": {
"challengeId": "007cfdcc-a46d-4340-a4c6-216ec2e4009c",
"expiresAt": "2019-08-24T14:15:22Z",
"attemptsRemaining": 5,
"resendCount": 0
}
}{
"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/auth/auth.controller.ts | 359–381 (resendOtp) |
| DTO (request) | apps/api-core/src/modules/auth/dto/resend-otp.dto.ts | 4–8 (ResendOtpDto) |
| DTO (response) | apps/api-core/src/modules/auth/dto/challenge.dto.ts | 7–22 (ChallengeDispatchResponseDto) |
| Service | apps/api-core/src/modules/auth/challenge.service.ts | resendOtp() |
| Prisma model | packages/prisma/prisma/schema.prisma | Challenge.resendCount, Challenge.codeHash, Challenge.attempts |