PIN
Flow Summary
The current PIN module has two groups:
Signed-in PIN Management
Step 1. Set PINPOST /api/v1/auth/set-pin
Step 2. Verify PINPOST /api/v1/auth/verify-pin
Step 3. Change PINPOST /api/v1/auth/change-pin
PIN Reset Flow
Step 1. Request reset OTPPOST /api/v1/auth/forgot-pin
Step 2. Verify OTPPOST /api/v1/auth/verify-otp
Step 3. Reset PINPOST /api/v1/auth/reset-pin
In development environments, OTP is currently hardcoded to 123456.
1. Set PIN
POST /api/v1/auth/set-pin
Treat this as a protected route and send:
Authorization: Bearer <access_token>
Request
{
"pin": "123456"
}
Success response
{
"status_code": 200,
"message": "PIN set successfully",
"data": null
}
Rules
pinmust be exactly 6 digits- this endpoint only works if the user does not already have a PIN
- if a PIN already exists, the service rejects the request and expects
change-pininstead
2. Verify PIN
POST /api/v1/auth/verify-pin
Treat this as a protected route and send:
Authorization: Bearer <access_token>
Request
{
"pin": "123456"
}
Success response
{
"status_code": 200,
"message": "OTP verified successfully",
"data": null
}
Notes
- The success message reuses the OTP verification locale string in the current handler.
- Wrong PIN returns
422, not401. - The service tracks failed attempts in Redis. After every 5 wrong attempts, the user is blocked for 1 minute and receives
429. - A successful verification clears the wrong-attempt counter.
3. Change PIN
POST /api/v1/auth/change-pin
Treat this as a protected route and send:
Authorization: Bearer <access_token>
Request
{
"current_pin": "123456",
"new_pin": "654321"
}
Success response
{
"status_code": 200,
"message": "PIN changed successfully",
"data": null
}
Rules
- both
current_pinandnew_pinmust be exactly 6 digits - the user must already have a PIN
current_pinmust match the stored PINnew_pinmust not be the same ascurrent_pin
4. Request PIN Reset OTP
POST /api/v1/auth/forgot-pin
This flow is designed for authenticated users and the handler checks the current mobile user context before it sends an OTP.
Request
{
"phone_code": "855",
"country_code": "KH",
"phone_number": "012345678"
}
You can also use:
{
"email": "user@example.com"
}
Success response
{
"status_code": 200,
"message": "OTP sent successfully",
"data": {
"session_id": "9b7f1b4d-7c75-4d14-bec8-0d03b0f809d6",
"expires_at": 600
}
}
Notes
phone_numberoremailis required.- The phone or email must belong to the currently authenticated user.
- The chosen phone/email must already be verified.
- The OTP session is stored in Redis for 10 minutes.
- Unlike
forgot-password, this endpoint does populateexpires_atwith600.
5. Verify OTP For PIN Reset
POST /api/v1/auth/verify-otp
Use the session_id returned by forgot-pin.
Request
{
"phone_code": "855",
"country_code": "KH",
"phone_number": "012345678",
"otp_code": "123456",
"session_id": "9b7f1b4d-7c75-4d14-bec8-0d03b0f809d6"
}
Email-based reset can send email instead of the phone fields.
Success response
{
"status_code": 200,
"message": "OTP verified successfully",
"data": {
"success": true,
"message": "OTP verified successfully",
"session_id": "2f264af8-98c4-4a3d-89bc-5958c5ba6931"
}
}
Use the returned session_id in reset-pin.
6. Reset PIN
POST /api/v1/auth/reset-pin
The Swagger annotation marks this as BearerAuth, but the current handler only validates the verified OTP session. The practical gate is possession of the session_id returned by verify-otp.
Request
{
"session_id": "2f264af8-98c4-4a3d-89bc-5958c5ba6931",
"new_pin": "654321"
}
Success response
{
"status_code": 200,
"message": "PIN reset successfully",
"data": {
"success": true,
"message": "PIN reset successfully"
}
}
Notes
new_pinmust be exactly 6 digits.- The service loads the verified OTP session from Redis, hashes the new PIN, updates the user record, and deletes the verification session.
- If the verification session is missing or expired, the service returns
400.
Integration Notes
- Use
set-pinonly once during first-time setup. - Use
verify-pinfor transaction confirmation or other sensitive confirmations that rely on the stored PIN. - For recovery, use the shared OTP step the same way as Password, but finish with
reset-pininstead ofreset-password.