Refresh Token
curl --request POST \
--url https://api.example.com/api/v1/auth/refresh-tokenimport requests
url = "https://api.example.com/api/v1/auth/refresh-token"
response = requests.post(url)
print(response.text)const options = {method: 'POST'};
fetch('https://api.example.com/api/v1/auth/refresh-token', 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/refresh-token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/auth/refresh-token"
req, _ := http.NewRequest("POST", url, nil)
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/refresh-token")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/auth/refresh-token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
response = http.request(request)
puts response.read_body{
"data.accessToken": "<string>"
}Authentication
Refresh Token
Get a new access token using refresh token
POST
/
api
/
v1
/
auth
/
refresh-token
Refresh Token
curl --request POST \
--url https://api.example.com/api/v1/auth/refresh-tokenimport requests
url = "https://api.example.com/api/v1/auth/refresh-token"
response = requests.post(url)
print(response.text)const options = {method: 'POST'};
fetch('https://api.example.com/api/v1/auth/refresh-token', 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/refresh-token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/auth/refresh-token"
req, _ := http.NewRequest("POST", url, nil)
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/refresh-token")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/auth/refresh-token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
response = http.request(request)
puts response.read_body{
"data.accessToken": "<string>"
}Endpoint
POST /api/v1/auth/refresh-token
http://localhost:3000/api/v1
This endpoint is public but requires a valid refresh token cookie.
Request
No request body is required. The refresh token is automatically sent via HTTP-only cookie.Important: You must include
credentials: 'include' in your fetch request to send the refresh
token cookie.Response
Success Response (200 OK)
interface RefreshTokenResponse {
success: boolean;
data: {
accessToken: string;
};
message: string;
meta: {
timestamp: string;
path: string;
method: string;
};
}
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"message": "Success",
"meta": {
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/refresh-token",
"method": "POST"
}
}
string
New JWT access token for authenticating API requests.
Important: Refresh Token Cookie
The refresh token is sent via HTTP-only cookie and is not included in the response body. A new
refresh token cookie is automatically set with each refresh.
- Name:
refreshToken - HttpOnly: true (cannot be accessed via JavaScript)
- Secure: true (in production, requires HTTPS)
- SameSite: Strict
- Max-Age: 24 hours (86400 seconds)
What Happens on Token Refresh
1
Cookie Validation
System reads refresh token from HTTP-only cookie
2
Token Verification
Validates refresh token signature and expiration
3
Token Type Check
Ensures token type is REFRESH (not ACCESS)
4
User Lookup
Retrieves user from database based on token payload (JobSeeker or Company)
5
Redis Validation
Checks if refresh token ID exists in Redis (not blacklisted/invalidated)
6
Invalidate Old Token
Removes old refresh token from Redis (token rotation security)
7
Generate New Tokens
Creates new access token and refresh token
8
Store New Refresh Token
Stores new refresh token ID in Redis and updates cookie
9
Return Access Token
Returns new access token in response body
Important: Refresh Token Rotation
Security Feature: Each time you refresh, the old refresh token is invalidated and a new one is
issued. This prevents refresh token reuse attacks.
- After calling this endpoint, your old refresh token becomes invalid
- A new refresh token is automatically set as a cookie
- You must use the new refresh token for subsequent refreshes
- Attempting to reuse an old refresh token will fail
Error Responses
401 Unauthorized - Refresh Token Not Found
Returned when refresh token cookie is missing.{
"success": false,
"error": {
"message": "Refresh token not found",
"statusCode": 401,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/refresh-token",
"method": "POST",
"details": "Unauthorized"
}
}
- User never logged in
- Cookie was cleared/expired
- Request doesn’t include
credentials: 'include' - CORS not configured to allow credentials
401 Unauthorized - Invalid Refresh Token
Returned when refresh token is invalid, expired, or already used.{
"success": false,
"error": {
"message": "Invalid refresh token",
"statusCode": 401,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/refresh-token",
"method": "POST",
"details": "Unauthorized"
}
}
- Token has expired (24 hours passed)
- Token signature is invalid
- Token has been used already (due to rotation)
- Token was manually invalidated/revoked
- User no longer exists in database
- Token not found in Redis (invalidated)
401 Unauthorized - Invalid Token Type
Returned when using access token instead of refresh token.{
"success": false,
"error": {
"message": "Invalid token type",
"statusCode": 401,
"timestamp": "2026-04-23T20:00:00.000Z",
"path": "/auth/refresh-token",
"method": "POST",
"details": "Unauthorized"
}
}
Token Lifecycle
| Event | Access Token | Refresh Token |
|---|---|---|
| Login/Verify | New token issued (1h) | New token issued (24h) |
| API Request | Sent in Authorization header | Sent as cookie |
| Access Token Expires | Becomes invalid | Still valid |
| Refresh Token Used | New token issued (1h) | Old invalidated, new issued (24h) |
| Refresh Token Expires | Can’t be refreshed | User must login |
| Logout | Cleared from client | Invalidated in Redis |
When to Refresh Tokens
On 401 Response (Reactive) - Recommended
On 401 Response (Reactive) - Recommended
Wait until you get a 401 error, then refresh the token and retry the request.Pros:
- Simple implementation
- No unnecessary refreshes
- Works with the examples above
- Brief disruption when token expires
Before Expiration (Proactive)
Before Expiration (Proactive)
Decode the JWT and refresh before it expires (e.g., 5 minutes before). Pros: - Seamless user
experience - No failed requests Cons: - Requires JWT decoding library - Timer management
complexity - May refresh unnecessarily
On App Load (Conservative)
On App Load (Conservative)
Always attempt to refresh on app initialization or page refresh.Pros:
- Ensures fresh token
- Simple to implement
- Extra request on every page load
- May refresh valid tokens
Common Issues
CORS / Cookie Issues
Problem: Refresh token cookie not being sent Solutions:- Ensure
credentials: 'include'is set in fetch - Check CORS settings allow credentials (
Access-Control-Allow-Credentials: true) - Verify cookie domain matches API domain (localhost for local dev)
- Check browser allows third-party cookies (if frontend/backend on different domains)
- In production, ensure frontend and backend are on same domain or properly configured
Token Rotation Confusion
Problem: Old refresh token doesn’t work after refresh This is expected behavior! Token rotation invalidates old tokens after use for security. Solution: Always use the latest refresh token cookie (browser handles this automatically)Infinite Refresh Loop
Problem: App keeps refreshing tokens in a loop Cause: Multiple requests triggering refresh simultaneously without queueing Solution: Implement refresh lock/queue (see TypeScript example above)Lost Cookie After Refresh
Problem: Refresh token cookie disappears after some time Cause: Cookie has 24-hour expiration Solution: This is normal - user needs to login again after 24 hoursSecurity Best Practices
Never store refresh tokens in localStorage or sessionStorage. They’re automatically handled
via HTTP-only cookies which cannot be accessed by JavaScript (protecting against XSS attacks).
Token Rotation: Old refresh tokens are automatically invalidated when new ones are issued,
preventing replay attacks if a token is intercepted.
Silent Refresh: Implement automatic token refresh in the background for seamless user
experience without interrupting their workflow.
Related Endpoints
- Login - Get initial tokens
- Verify Email - Get tokens after email verification
- Register Job Seeker - Create job seeker account
- Register Company - Create company account
