Verify Email
curl --request POST \
--url https://api.example.com/api/v1/auth/verify-email \
--header 'Content-Type: application/json' \
--data '
{
"email": "<string>",
"code": "<string>"
}
'import requests
url = "https://api.example.com/api/v1/auth/verify-email"
payload = {
"email": "<string>",
"code": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: '<string>', code: '<string>'})
};
fetch('https://api.example.com/api/v1/auth/verify-email', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/auth/verify-email",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => '<string>',
'code' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/auth/verify-email"
payload := strings.NewReader("{\n \"email\": \"<string>\",\n \"code\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/auth/verify-email")
.header("Content-Type", "application/json")
.body("{\n \"email\": \"<string>\",\n \"code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/auth/verify-email")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"<string>\",\n \"code\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"id": "<string>",
"email": "<string>",
"firstName": "<string>",
"lastName": "<string>",
"profileImageUrl": {},
"isActive": true,
"isVerified": true,
"lastLoginAt": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>",
"accessToken": "<string>",
"name": "<string>",
"description": {},
"logoUrl": {},
"industry": "<string>",
"size": "<string>",
"type": "<string>",
"headquartersLocation": {},
"foundedYear": {},
"websiteUrl": {},
"benefits": {},
"linkedIn": {},
"facebook": {},
"twitter": {}
},
"message": "<string>",
"meta": {
"timestamp": "<string>",
"path": "<string>",
"method": "<string>"
}
}Authentication
Verify Email
Verify email address with OTP code
POST
/
api
/
v1
/
auth
/
verify-email
Verify Email
curl --request POST \
--url https://api.example.com/api/v1/auth/verify-email \
--header 'Content-Type: application/json' \
--data '
{
"email": "<string>",
"code": "<string>"
}
'import requests
url = "https://api.example.com/api/v1/auth/verify-email"
payload = {
"email": "<string>",
"code": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: '<string>', code: '<string>'})
};
fetch('https://api.example.com/api/v1/auth/verify-email', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/auth/verify-email",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => '<string>',
'code' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/auth/verify-email"
payload := strings.NewReader("{\n \"email\": \"<string>\",\n \"code\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/auth/verify-email")
.header("Content-Type", "application/json")
.body("{\n \"email\": \"<string>\",\n \"code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/auth/verify-email")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"<string>\",\n \"code\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"id": "<string>",
"email": "<string>",
"firstName": "<string>",
"lastName": "<string>",
"profileImageUrl": {},
"isActive": true,
"isVerified": true,
"lastLoginAt": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>",
"accessToken": "<string>",
"name": "<string>",
"description": {},
"logoUrl": {},
"industry": "<string>",
"size": "<string>",
"type": "<string>",
"headquartersLocation": {},
"foundedYear": {},
"websiteUrl": {},
"benefits": {},
"linkedIn": {},
"facebook": {},
"twitter": {}
},
"message": "<string>",
"meta": {
"timestamp": "<string>",
"path": "<string>",
"method": "<string>"
}
}Endpoint
POST /api/v1/auth/verify-email
http://localhost:3000/api/v1
This endpoint is public and does not require authentication.
Request Body
string
required
The email address to verify (must match the registered email).
string
required
The 6-digit OTP code sent to the email. Must be exactly 6 numeric digits.
Request Shape
interface VerifyEmailDto {
email: string;
code: string;
}
Response
Success Response (200 OK)
TypeScript Types
interface JobSeekerVerifyResponse {
success: boolean;
data: {
id: string;
email: string;
firstName: string;
lastName: string;
profileImageUrl: string | null;
isActive: boolean;
isVerified: boolean;
lastLoginAt: string;
createdAt: string;
updatedAt: string;
accessToken: string;
};
message: string;
meta: {
timestamp: string;
path: string;
method: string;
};
}
interface CompanyVerifyResponse {
success: boolean;
data: {
id: string;
email: string;
name: string;
description: string | null;
logoUrl: string | null;
industry: string;
size: CompanySize;
type: CompanyType;
headquartersLocation: string | null;
foundedYear: number | null;
websiteUrl: string | null;
benefits: string | null;
linkedIn: string | null;
facebook: string | null;
twitter: string | null;
isActive: boolean;
isVerified: boolean;
createdAt: string;
updatedAt: string;
accessToken: string;
};
message: string;
meta: {
timestamp: string;
path: string;
method: string;
};
}
For Job Seeker
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"profileImageUrl": null,
"isActive": true,
"isVerified": true,
"lastLoginAt": "2024-01-15T10:30:00.000Z",
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"meta": {
"timestamp": "2024-01-15T10:30:00.000Z",
"path": "/auth/verify-email",
"method": "POST"
},
"message": "Email verified successfully. You are now logged in."
}
For Company
{
"success": true,
"data": {
"id": "660f9500-f39c-52e5-b827-557766551111",
"email": "hr@techcorp.com",
"name": "TechCorp Inc.",
"description": null,
"logoUrl": null,
"industry": "Technology",
"size": "SIZE_51_200",
"type": "STARTUP",
"headquartersLocation": null,
"foundedYear": null,
"websiteUrl": null,
"benefits": null,
"linkedIn": null,
"facebook": null,
"twitter": null,
"isActive": true,
"isVerified": true,
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"meta": {
"timestamp": "2024-01-15T10:30:00.000Z",
"path": "/auth/verify-email",
"method": "POST"
},
"message": "Email verified successfully. You are now logged in."
}
boolean
Indicates if the verification was successful.
object
Contains the complete user/company data and access token.
Show Job Seeker Fields
Show Job Seeker Fields
string
Unique user identifier (UUID format).
string
User’s email address.
string
User’s first name.
string
User’s last name.
string | null
URL to user’s profile image.
null if not set.boolean
Whether the account is active. Always
true after verification.boolean
Whether the email is verified. Always
true after successful verification.string
ISO 8601 timestamp of this verification/login.
string
ISO 8601 timestamp of account creation.
string
ISO 8601 timestamp of last update.
string
JWT access token for authenticating API requests. Valid for 1 hour (3600 seconds).
Show Company Fields
Show Company Fields
string
Unique company identifier (UUID format).
string
Company’s email address.
string
Company name.
string | null
Company description.
null if not set.string | null
URL to company logo.
null if not set.string
Company industry.
string
Company size:
SIZE_1_50, SIZE_51_200, SIZE_201_1000, or SIZE_1000_PLUS.string
Company type:
STARTUP, SCALE_UP, ENTERPRISE, NON_PROFIT, or GOVERNMENT.string | null
Company headquarters location.
null if not set.number | null
Year the company was founded.
null if not set.string | null
Company website URL.
null if not set.string | null
Company benefits description.
null if not set.string | null
LinkedIn profile URL.
null if not set.string | null
Facebook page URL.
null if not set.string | null
Twitter profile URL.
null if not set.boolean
Whether the account is active. Always
true after verification.boolean
Whether the email is verified. Always
true after successful verification.string
ISO 8601 timestamp of account creation.
string
ISO 8601 timestamp of last update.
string
JWT access token for authenticating API requests. Valid for 1 hour (3600 seconds).
string
Success message: “Email verified successfully. You are now logged in.”
object
Important: Refresh Token Cookie
The refresh token is automatically set as an HTTP-only cookie named
refreshToken. You
MUST use credentials: 'include' in your fetch requests to enable cookie handling.- Name:
refreshToken - HttpOnly:
true(cannot be accessed via JavaScript) - Secure:
true(in production, requires HTTPS) - SameSite:
Strict - Max-Age: 86400 seconds (24 hours)
credentials: 'include'.
What Happens on Verification
1
OTP Validation
System verifies the OTP code against the stored value in Redis.
2
User Type Detection
System determines if the user is a Job Seeker or Company based on stored OTP data.
3
Email Status Update
User’s
isVerified field is set to true in the database.4
Login Timestamp Update
For job seekers,
lastLoginAt is updated to current timestamp.5
OTP Deletion
Used OTP is immediately deleted from Redis.
6
Token Generation
New JWT access and refresh tokens are generated.
7
Refresh Token Storage
Refresh token is stored in Redis with 24-hour expiration and set as HTTP-only cookie.
8
User Logged In
User is now authenticated and can access protected endpoints.
Error Responses
400 Bad Request - Validation Error
Returned when request validation fails.{
"success": false,
"error": {
"message": ["Invalid email format", "OTP must be exactly 6 digits"],
"statusCode": 400,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/verify-email",
"method": "POST",
"details": "Bad Request"
}
}
Invalid email format- Email is not in valid formatOTP must be exactly 6 digits- Code is not exactly 6 charactersOTP must contain only numbers- Code contains non-numeric characters
401 Unauthorized - Invalid OTP
Returned when OTP is incorrect or expired.{
"success": false,
"error": {
"message": "Invalid or expired OTP code",
"statusCode": 401,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/verify-email",
"method": "POST",
"details": "Unauthorized"
}
}
- OTP code is incorrect
- OTP has expired (10 minutes validity)
- OTP was already used
- Email doesn’t match registration email
- OTP was never generated for this email
401 Unauthorized - User Not Found
Returned when no user exists with the provided email.{
"success": false,
"error": {
"message": "User not found",
"statusCode": 401,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/verify-email",
"method": "POST",
"details": "Unauthorized"
}
}
401 Unauthorized - Verification Failed
Generic error for other verification issues.{
"success": false,
"error": {
"message": "Email verification failed",
"statusCode": 401,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/verify-email",
"method": "POST",
"details": "Unauthorized"
}
}
Error responses follow the global API envelope:
success: false and an error object containing
message, statusCode, timestamp, path, method, and details.Validation Rules
| Field | Type | Required | Rules |
|---|---|---|---|
| string | Yes | Must be valid email format | |
| code | string | Yes | Must be exactly 6 digits, numeric only (0-9) |
OTP Behavior
OTP Validity: Each OTP is valid for 10 minutes from generation. After expiration, users need
to request a new code (feature in development).
Single Use: Each OTP can only be used once. After successful verification, the OTP is
immediately deleted from Redis.
Case Insensitive Email: Email comparison is case-insensitive, so
john@example.com and
JOHN@example.com are treated as the same.After Verification
Once email is verified, the user:- ✅ Is automatically logged in
- ✅ Receives access token (1 hour validity)
- ✅ Has refresh token stored in HTTP-only cookie (24 hours validity)
- ✅ Can access all protected endpoints
- ✅ Can login normally without re-verification
- ✅ Has
isVerified: truein their account
Related Endpoints
- Register Job Seeker - Create job seeker account
- Register Company - Create company account
- Login - Login after verification
- Refresh Token - Get new access token
