Authentication
The Batchmates API supports two authentication methods: JWT tokens for mobile and stateless clients, and session-based authentication for web admin interfaces.
Always include Accept: application/json in every request. Without it, Laravel returns an HTML redirect (302) instead of a JSON error response — this is the most common integration mistake.
Authentication Methods
JWT Tokens (Mobile)
Stateless token-based authentication for mobile apps and third-party integrations.
Best for:
- Mobile applications
- Third-party integrations
- Stateless API clients
Features:
- 30-minute access token expiration
- 6-month refresh token for seamless renewal
- No server-side session storage
Session-based (Web)
Cookie-based authentication for browser applications.
Best for:
- Web admin panels
- Browser-based dashboards
Features:
- Automatic cookie handling
- No tokens returned
- Server session lifetime
JWT Authentication (Mobile)
Base URL: https://batchmates-v2.revlv.com/api/v1/mobile/auth
Register
Creates a new user account and sends a 6-digit verification code to their email.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/register \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"first_name": "Juan",
"last_name": "Dela Cruz",
"email": "donor@example.com",
"password": "your-password",
"password_confirmation": "your-password",
"device_name": "iPhone 15"
}'
Request Body
- Name
first_name- Type
- string
- Description
User's first name (max 255 characters)
- Name
last_name- Type
- string
- Description
User's last name (max 255 characters)
- Name
email- Type
- string
- Description
Must be unique across all users
- Name
password- Type
- string
- Description
Minimum 8 characters
- Name
password_confirmation- Type
- string
- Description
Must match
password
- Name
device_name- Type
- string
- Description
Device name for identification (max 255 characters)
- Name
device_token- Type
- string
- Description
Push notification token (max 500 characters)
Response 201
{
"success": true,
"data": {
"id": 5,
"email": "donor@example.com",
"email_verified": false,
"can_resend_at": "2026-03-02T10:01:00.000000Z"
},
"message": "Registration successful. Please check your email for verification code."
}
The email contains a 6-digit numeric code (e.g. 482910) — not a clickable link. The code expires in 15 minutes.
Verify Email
Verifies the 6-digit code. On success, the user is immediately logged in — no separate login step required.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/verify-email \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "donor@example.com",
"code": "482910",
"device_name": "iPhone 15"
}'
Request Body
- Name
email- Type
- string
- Description
The email address being verified
- Name
code- Type
- string
- Description
6-digit code from the verification email. Field name is
code, nottoken.
- Name
device_name- Type
- string
- Description
Device name used to identify the session. Required for mobile.
Response 200
{
"success": true,
"data": {
"user": {
"id": 5,
"first_name": "Juan",
"last_name": "Dela Cruz",
"name": "Juan Dela Cruz",
"email": "donor@example.com",
"phone": null,
"avatar": null,
"status": "active",
"email_verified": true,
"roles": ["donor"],
"permissions": [],
"created_at": "2026-03-02T10:00:00.000000Z",
"updated_at": "2026-03-02T10:00:00.000000Z"
},
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "def50200e8d...",
"token_type": "bearer",
"expires_in": 1800
},
"message": "Email verified successfully. You are now logged in."
}
Resend Verification Code
Sends a new 6-digit code. A 60-second cooldown is enforced between requests.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/resend-verification \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "email": "donor@example.com" }'
Response 200
{
"success": true,
"message": "Verification code sent successfully.",
"can_resend_at": "2026-03-02T10:02:00.000000Z"
}
Response 429 — Cooldown active
{
"success": false,
"message": "Please wait 47 seconds before requesting a new code.",
"can_resend_at": "2026-03-02T10:01:47.000000Z",
"seconds_left": 47
}
Check Verification Status
Check whether an email has been verified and whether a resend is currently allowed.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/check-verification-status \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "email": "donor@example.com" }'
Response 200
{
"success": true,
"data": {
"email_verified": false,
"can_resend": true,
"seconds_until_resend": 0
}
}
Login
Authenticates an existing verified user and returns JWT tokens.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "donor@example.com",
"password": "your-password",
"device_name": "iPhone 15"
}'
Request Body
- Name
email- Type
- string
- Description
User's email address
- Name
password- Type
- string
- Description
User's password
- Name
device_name- Type
- string
- Description
Device name for session identification. Required for mobile — omitting returns a 422 error.
- Name
device_token- Type
- string
- Description
Push notification token (max 500 characters)
Response 200
{
"success": true,
"data": {
"user": {
"id": 5,
"first_name": "Juan",
"last_name": "Dela Cruz",
"name": "Juan Dela Cruz",
"email": "donor@example.com",
"phone": null,
"avatar": null,
"status": "active",
"email_verified": true,
"roles": ["donor"],
"permissions": [],
"last_login_at": "2026-03-02T10:00:00.000000Z"
},
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "def50200e8d...",
"token_type": "bearer",
"expires_in": 1800
},
"message": "Logged in successfully"
}
- Name
access_token- Type
- string
- Description
JWT for API authentication. Valid for 30 minutes (
expires_in: 1800).
- Name
refresh_token- Type
- string
- Description
Used to get a new access token. Valid for 6 months.
- Name
token_type- Type
- string
- Description
Always
"bearer"
Using the Access Token
Include the JWT in every authenticated request:
curl https://batchmates-v2.revlv.com/api/v1/profile \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..." \
-H "Accept: application/json"
const response = await fetch('https://batchmates-v2.revlv.com/api/v1/profile', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
},
})
Refresh Token
When the access token expires after 30 minutes, use the refresh token to get a new one.
The refresh token goes in the request body — not the Authorization header.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/refresh \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "refresh_token": "def50200e8d..." }'
Response 200
{
"success": true,
"data": {
"user": { "..." },
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "bearer",
"expires_in": 1800
}
}
The response does not include a new refresh token. The original refresh token remains valid for 6 months from when it was issued.
Logout
Invalidates the current access and refresh tokens.
curl -X POST https://batchmates-v2.revlv.com/api/v1/mobile/auth/logout \
-H "Authorization: Bearer {access_token}" \
-H "Accept: application/json"
Response 200
{ "success": true, "message": "Logged out successfully" }
Session Authentication (Web)
Base URL: https://batchmates-v2.revlv.com/api/v1/web/auth
Session-based auth uses HTTP-only cookies. No tokens are returned — the browser stores and sends the cookie automatically.
Register
curl -X POST https://batchmates-v2.revlv.com/api/v1/web/auth/register \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"first_name": "Juan",
"last_name": "Dela Cruz",
"email": "admin@institution.edu",
"password": "your-password",
"password_confirmation": "your-password"
}'
Response 201
{
"success": true,
"data": {
"id": 10,
"email": "admin@institution.edu",
"email_verified": false,
"can_resend_at": "2026-03-02T10:01:00.000000Z"
},
"message": "Registration successful. Please check your email for verification code."
}
Verify Email
curl -X POST https://batchmates-v2.revlv.com/api/v1/web/auth/verify-email \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-c cookies.txt \
-d '{
"email": "admin@institution.edu",
"code": "482910"
}'
Field name is code, not token. On success the session cookie is set automatically — no separate login step needed.
Response 200
{
"success": true,
"data": {
"id": 10,
"first_name": "Juan",
"last_name": "Dela Cruz",
"name": "Juan Dela Cruz",
"email": "admin@institution.edu",
"status": "active",
"email_verified": true,
"roles": ["institution_admin"],
"permissions": []
},
"message": "Email verified successfully. You are now logged in."
}
Resend Verification Code
curl -X POST https://batchmates-v2.revlv.com/api/v1/web/auth/resend-verification \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "email": "admin@institution.edu" }'
Response 200
{
"success": true,
"message": "Verification code sent successfully.",
"can_resend_at": "2026-03-02T10:02:00.000000Z"
}
Check Verification Status
curl -X POST https://batchmates-v2.revlv.com/api/v1/web/auth/check-verification-status \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "email": "admin@institution.edu" }'
Response 200
{
"success": true,
"data": {
"email_verified": false,
"can_resend": true,
"seconds_until_resend": 0
}
}
Login
curl -X POST https://batchmates-v2.revlv.com/api/v1/web/auth/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-c cookies.txt \
-d '{
"email": "admin@institution.edu",
"password": "your-password"
}'
Response 200
{
"success": true,
"data": {
"id": 10,
"first_name": "Juan",
"last_name": "Dela Cruz",
"name": "Juan Dela Cruz",
"email": "admin@institution.edu",
"institution_id": 1,
"institution": { "id": 1, "name": "Xavier University" },
"status": "active",
"email_verified": true,
"roles": ["institution_admin"],
"permissions": [],
"last_login_at": "2026-03-02T10:00:00.000000Z"
},
"message": "Logged in successfully"
}
No tokens in the response. The session cookie is set automatically by the browser.
Using Sessions
Include credentials: 'include' so the cookie is sent automatically:
const response = await fetch('https://batchmates-v2.revlv.com/api/v1/campaigns', {
credentials: 'include',
headers: { 'Accept': 'application/json' },
})
# With cURL, use the cookie file saved during login
curl https://batchmates-v2.revlv.com/api/v1/campaigns \
-H "Accept: application/json" \
-b cookies.txt
Logout
curl -X POST https://batchmates-v2.revlv.com/api/v1/web/auth/logout \
-H "Accept: application/json" \
-b cookies.txt
Response 200
{ "success": true, "message": "Logged out successfully" }
Role-Based Access Control
- Name
system_admin- Description
Full access across all institutions — manage users, campaigns, institutions, and system settings
- Name
institution_admin- Description
Manage campaigns, users, bank accounts, and committees within their institution
- Name
committee_member- Description
Approve and reject campaigns, manage withdrawals, view institution data
- Name
donor- Description
Browse campaigns, create donations, manage their own profile
Permission Matrix
| Permission | system_admin | institution_admin | committee_member | donor |
|---|---|---|---|---|
| Manage institutions | ✓ | — | — | — |
| Manage users | ✓ | ✓ | — | — |
| Suspend users | ✓ | ✓ | — | — |
| View campaigns | ✓ | ✓ | ✓ | ✓ |
| Create campaigns | ✓ | ✓ | — | ✓ |
| Approve/reject campaigns | ✓ | ✓ | ✓ | — |
| Create donations | ✓ | — | — | ✓ |
| View donations | ✓ | ✓ | ✓ | — |
| View withdrawals | ✓ | ✓ | ✓ | — |
| Create withdrawals | ✓ | ✓ | ✓ | — |
| Approve withdrawals | ✓ | — | ✓ | — |
| View audit logs | ✓ | ✓ | — | — |
Roles are returned in every auth response under "roles": ["role_name"]. The permissions array is always empty — permissions are enforced server-side only.
Password Reset
Base URL: https://batchmates-v2.revlv.com/api/v1/auth
These endpoints are unauthenticated and work for both mobile and web clients.
Password reset uses a 3-step OTP flow: request a code → verify the code → submit new password. No deep links or email links are used.
Forgot Password
Generates a 6-digit OTP and emails it to the user. Always returns a generic success response to prevent email enumeration.
curl -X POST https://batchmates-v2.revlv.com/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "email": "donor@example.com" }'
Request Body
- Name
email- Type
- string
- Description
The email address associated with the account
Response 200
{
"message": "If that email exists, a reset code has been sent"
}
The email contains a 6-digit numeric OTP valid for 15 minutes. The OTP is stored as a SHA-256 hash in password_reset_tokens. Each new request resets the code and clears attempt counters.
Verify Reset Token
Validates the 6-digit OTP before showing the new password form. Use this as step 2 to confirm the code is correct and unexpired before asking the user to enter a new password.
curl -X POST https://batchmates-v2.revlv.com/api/v1/auth/verify-reset-token \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "donor@example.com",
"token": "123456"
}'
Request Body
- Name
email- Type
- string
- Description
The email address associated with the account
- Name
token- Type
- string
- Description
The 6-digit OTP from the email (exactly 6 numeric characters)
Response 200 — OTP valid
{ "valid": true, "message": "Token is valid" }
Response 422 — OTP invalid, expired, or locked out
{ "valid": false, "message": "Invalid code. 3 attempts remaining." }
{ "valid": false, "message": "Too many failed attempts. Please request a new code." }
Security rules:
- Maximum 5 attempts per OTP — exceeded attempts lock out the token entirely
- OTP expires after 15 minutes
- Failed attempts are counted and returned in the message
Reset Password
Resets the password using the verified 6-digit OTP. The OTP is re-validated on this request as well.
curl -X POST https://batchmates-v2.revlv.com/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "donor@example.com",
"token": "123456",
"password": "newpassword456",
"password_confirmation": "newpassword456"
}'
Request Body
- Name
email- Type
- string
- Description
The email address associated with the account
- Name
token- Type
- string
- Description
The 6-digit OTP from the email
- Name
password- Type
- string
- Description
New password (minimum 8 characters)
- Name
password_confirmation- Type
- string
- Description
Must match
password
Response 200
{ "message": "Password has been reset successfully" }
Response 422 — Invalid OTP or validation error
{ "message": "Invalid or expired reset code." }
On success, the password_reset_tokens row is deleted and the user's password is updated immediately.
Common Authentication Errors
| Status | Meaning |
|---|---|
400 | Wrong or expired verification code |
401 | Missing or invalid token / session |
403 | Unverified email; suspended account; insufficient role |
422 | Validation error — missing required field |
429 | Resend cooldown active; rate limit hit |
// 403 — email not verified
{
"success": false,
"message": "Please verify your email before logging in.",
"requires_verification": true,
"email": "donor@example.com"
}
// 422 — missing device_name (mobile)
{
"message": "The given data was invalid.",
"errors": {
"device_name": ["The device name field is required."]
}
}
JWT vs Session — Key Differences
| JWT (Mobile) | Session (Web) | |
|---|---|---|
| Tokens returned | access_token + refresh_token | None |
| Auth header | Authorization: Bearer <token> | N/A |
| Cookie | None | HTTP-only session cookie |
| Access token expiry | 30 minutes | Server session lifetime |
| Refresh token expiry | 6 months | N/A |
device_name | Required for login + verify | Not required |
| Refresh endpoint | POST /mobile/auth/refresh | N/A |
Security Notes
OTP / Verification Code Storage
Verification codes (6-digit email codes) are stored as SHA-256 hashes — never in plaintext. After 5 consecutive failed verification attempts, the code is automatically invalidated and a new one must be requested.
JWT Token Invalidation
Each user has a token_version counter embedded in every issued JWT. The ValidateJwtVersion middleware rejects tokens whose embedded version is lower than the current value.
POST /api/v1/mobile/auth/logout-all increments token_version, immediately invalidating all active tokens for the user across all devices. Use this when a device is lost or compromised.
Password Reset Security (Web Only)
The web reset-password page clears the token and email query params from the browser's URL immediately after reading them, so they do not persist in browser history or appear in referrer headers. This is a web-client-only behaviour — the mobile deep link flow is unaffected.