Token-based REST API for the mobile app. Every response is JSON and uses the same envelope. Authentication is a Sanctum bearer token returned by register and login.
A single admin setting (Settings → OTP → “OTP Verification Enabled”) changes how two flows behave. Read GET /app-config at app start and branch on data.otp.enabled — never hard-code either flow.
This server right now:
OTP is ON
— registration is
otp_verification,
forgot-password is
otp_verification.
When OTP is ON
Register is two steps: POST /auth/register sends an SMS OTP and returns a verification_id; the account is created only by POST /auth/register/verify-otp.
Forgot password works: POST /auth/forgot-password → POST /auth/forgot-password/verify-otp → POST /auth/reset-password.
POST /auth/resend-otp is available for both flows.
When OTP is OFF
Register is one step: POST /auth/register creates the account and returns the access token straight away. No SMS is sent and no verification_id is returned.
Forgot password is unavailable: every forgot-password / verify / reset endpoint answers 403 with error_code OTP_DISABLED. Hide the “Forgot password?” link.
POST /auth/resend-otp also answers 403 OTP_DISABLED.
An authenticated user can still change their password with POST /auth/change-password, since the current password is proof enough.
Auth Flows
The exact call sequence for each case. Read GET /app-config first and pick the branch from data.flows.
Registration — OTP ON
1
POST /auth/register
Send name, mobile, password, password_confirmation (+ optional email and device fields). Response: otp_required: true and a verification_id. No account exists yet.
2
POST /auth/register/verify-otp
Send verification_id + otp. The account is created and a token is returned. Optionally re-send the device fields here.
↻
POST /auth/resend-otp
If the SMS does not arrive. Respect resend_available_in_seconds before enabling the button.
Registration — OTP OFF
1
POST /auth/register
Same request body. The account is created immediately and the response carries otp_required: false plus the token. Skip the OTP screen entirely.
Forgot Password — OTP ON
1
POST /auth/forgot-password
Send the registered mobile. Returns a verification_id.
2
POST /auth/forgot-password/verify-otp
Send verification_id + otp. Returns a single-use reset_token valid for 15 minutes.
3
POST /auth/reset-password
Send reset_token + password + password_confirmation. All existing tokens are revoked, so send the user back to login.
Forgot Password — OTP OFF
×
Not available
Every forgot-password endpoint answers 403 with
error_code: OTP_DISABLED. Hide the entry point in the UI
(check data.flows.forgot_password === 'disabled').
A signed-in user can still use POST /auth/change-password,
and an admin can generate a new password from the panel.
Request & Response Format
Every endpoint speaks JSON and returns the same envelope, so one parser handles the whole API.
Required headers
Header
Value
Accept
application/json (required — without it Laravel may answer with HTML)
Content-Type
application/json
Authorization
Bearer {token} (only on authenticated endpoints)
Success envelope
{
"success": true,
"message": "Human-readable text, safe to show in the UI.",
"data": "{ ... } // present when the endpoint returns a payload"
}
Error envelope
{
"success": false,
"message": "Human-readable text, safe to show in the UI.",
"error_code": "MACHINE_READABLE_CODE",
"errors": "{ \"field\": [\"message\"] } // only on 422 validation failures"
}
Authentication
Register and login both return data.token. Send it on every protected call as Authorization: Bearer {token}.
Tokens do not expire on a timer. They are revoked by logout, logout-all, a password reset, or the admin deactivating or deleting the account.
A revoked token returns 401 UNAUTHENTICATED; a deactivated account returns 403 ACCOUNT_INACTIVE. Treat both as "go back to the login screen".
Store the token in secure storage (iOS Keychain / Android EncryptedSharedPreferences), never in plain preferences or a log.
Mobile number format
Canonical form is 10 digits starting with 6–9, e.g. 9876543210.
The server strips separators and a leading +91, 91 or 0 before validating, so +91 98765-43210 is accepted and stored as 9876543210.
Error Codes
Branch on error_code, never on the message text — messages may be reworded.
error_code
HTTP
Meaning & what the app should do
VALIDATION_ERROR
422
One or more fields failed validation. Read the errors object for per-field messages.
INVALID_CREDENTIALS
401
Wrong mobile number or password. Deliberately the same message for both so registered numbers cannot be probed.
UNAUTHENTICATED
401
Missing, malformed or revoked bearer token. Send the user back to login.
ACCOUNT_INACTIVE
403
The admin deactivated this account. All its tokens were revoked; message carries the reason.
OTP_DISABLED
403
OTP is switched off in the admin panel and the requested flow needs it.
OTP_INVALID
422
Wrong code. attempts_remaining tells you how many tries are left.
OTP_EXPIRED
410
The code aged out. Start the flow again.
OTP_MAX_ATTEMPTS
429
Too many wrong codes. The verification was destroyed; request a fresh OTP.
OTP_COOLDOWN
429
Asked for a new OTP too soon. retry_after holds the seconds to wait.
OTP_RESEND_LIMIT
429
Resend limit for this verification reached. Restart the flow.
OTP_SEND_FAILED
502
The SMS gateway rejected or could not take the message. Offer a retry.
OTP_ALREADY_VERIFIED
409
This verification_id was already confirmed.
OTP_PURPOSE_MISMATCH
422
A registration verification_id was used on a forgot-password endpoint, or vice versa.
VERIFICATION_NOT_FOUND
404
Unknown or already-consumed verification_id.
RESET_TOKEN_INVALID
422
The reset_token is wrong or has already been used.
RESET_TOKEN_EXPIRED
422
The reset_token is older than 15 minutes. Verify the OTP again.
USER_NOT_FOUND
404
No account is registered with that mobile number.
MOBILE_TAKEN
409
The number was registered by someone else while the OTP was in flight.
EMAIL_TAKEN
409
The email was taken by another account while the OTP was in flight.
TOO_MANY_ATTEMPTS
429
Login throttle tripped (5 failures per mobile+IP, 5-minute lockout).
MAINTENANCE_MODE
503
The admin put the API in maintenance mode. Only /app-config stays reachable.
ENDPOINT_NOT_FOUND
404
Wrong URL or HTTP method.
SERVER_ERROR
500
Unexpected server fault. Details are only included while APP_DEBUG is on.
Validation error shape (422)
{
"success": false,
"message": "Enter a valid 10-digit Indian mobile number.",
"error_code": "VALIDATION_ERROR",
"errors": {
"mobile": [
"Enter a valid 10-digit Indian mobile number."
],
"password": [
"The password field must be at least 8 characters."
]
}
}
message is the first error, ready to show in a toast. errors maps each field to its messages for inline display.
App Config
Call this first, on every app launch. It tells the client which auth flows are live.
Get App Config
GET/api/v1/app-config
PublicRate limit: 60 / minute
Returns the OTP switch state, the flow the client should use, Firebase client keys, version gating and the maintenance flag. This endpoint stays reachable during maintenance mode.
Parameters
None.
cURL
curl -X GET 'https://dpbossapp.io/api/v1/app-config' \
-H 'Accept: application/json'
flows.registration is "otp_verification" or "direct".
flows.forgot_password is "otp_verification" or "disabled".
Possible error codes
MAINTENANCE_MODE is never returned here.
Markets & Games
The game catalogue: markets, their timings, results, payout rates and the panna lookups. All public, so the app can render its home screen before the user signs in.
List Markets
GET/api/v1/markets
PublicRate limit: 120 / minute
Every active market for one engine, with its open and close times, whether each session is still taking bids, and the result for the current gaming day. One endpoint covers all three engines -- pass `engine` to pick which.
status is one of: open (both sessions), close_only (open time has passed), finished (close time has passed), holiday (market shut today), suspended (admin paused it), closed (betting switched off globally).
Drive the bid buttons from accepts_open_bids / accepts_close_bids, not from the times -- they already account for the holiday, pause and global switches.
gaming_date is the betting day, which only rolls over at the configured time (06:00 by default). Before that the previous day results are still shown.
Undeclared results come back as *** and *, ready to display as-is.
Possible error codes
VALIDATION_ERRORMAINTENANCE_MODE
Get One Market
GET/api/v1/markets/{slug}
PublicRate limit: 120 / minute
A single market in the same shape as the list entry. Answers 404 if the market is inactive.
requires_session marks the bets placed against the open or close side. Jodi and the sangam bets need both results, so they carry no session.
Payout scales with the stake: at "10 ka 95", a 50 stake returns 475.
Possible error codes
VALIDATION_ERRORMAINTENANCE_MODE
Panna Lookup
GET/api/v1/panna
PublicRate limit: 120 / minute
The panna reference list, grouped by type. Used by the bulk bid screens to expand a shorthand selection into the actual numbers. With no filters it returns all 220.
Parameters
—
sent as query string
Parameter
Type
Required
Validation
Description
types[]
string
Optional
nullable, array, each in:sp,dp,tp
Repeat the key to pass several, e.g. types[]=sp&types[]=dp. Defaults to all three.
Placing bets and reading them back. One submission is all-or-nothing: every bid in it is validated first, then the whole lot is stored and charged as a single wallet debit. A bad bid anywhere in the list means nothing is stored and the wallet is untouched.
Place Bids
POST/api/v1/bids
Bearer token requiredRate limit: 60 / minute
Takes one or more bids for a single market. Send the whole screenful in one call rather than one request per bid. `session` is required for games that are played per session (single digit and the pannas) and must be omitted for jodi and the sangams, which are decided by both results and therefore close when the open session closes.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
market
string
Required
required, must be an existing market slug
The market slug, from GET /markets.
Example: kalyan
bids
array
Required
required, array, min:1, max per the Max Bids per Submission setting (100 by default)
The bids to place. All of them must be for the market named above.
Example: [ { ... } ]
bids[].game_type
string
Required
required, must be an active game on this market engine
The game code from GET /game-rates, for example single, jodi, single_panna, half_sangam.
Example: single_panna
bids[].bid_number
string
Required
required, shape depends on the game -- see the notes below
The number staked. Sent as a string so leading zeros survive.
Example: 123
bids[].amount
number
Required
required, numeric, between the Minimum Bid and Maximum Bid settings
The stake for this one bid. Read the current limits from GET /app-config rather than hard-coding them.
Example: 50
bids[].session
string
Sometimes
nullable, in:open,close -- required for single digit and the pannas on a main market
Which side of the market this bid is for. Omit it for jodi, half sangam and full sangam.
{
"success": false,
"message": "Bid 2: 133 is a double panna, not a single panna.",
"error_code": "BID_NUMBER_INVALID",
"bid_index": 1
}
Notes
single -- one digit, 0 to 9. Example: 5
jodi -- two digits, 00 to 99. Example: 57
single_panna / double_panna / triple_panna -- a three digit panna that really exists and is of that type. 123 is a single panna and is refused as a double. Use GET /panna to list the valid ones.
half_sangam -- a panna and a digit, either way round: "123-7" is open panna with close digit, "3-457" is open digit with close panna.
full_sangam -- open panna and close panna: "123-457".
The signed-in player's own bids, newest first, with a summary over whatever the filters select. A bid stays `pending` until the result it depends on is declared, then becomes `won` or `lost` by itself.
Balance, history, and the two ways money moves. Deposits are claims until an admin confirms them. A withdrawal leaves the wallet the moment it is raised, not when it is approved, so the same balance cannot also be staked while a payout is pending.
Wallet Balance
GET/api/v1/wallet
Bearer token requiredRate limit: 120 / minute
The current balance, the two feature flags, and how many requests are still waiting.
Every movement, newest first. Each row carries `signed_amount`, negative for a debit, and `balance_after`, so a list can be rendered without branching on the type and a running balance never has to be worked out in the app.
What the pay-in screen needs. `upi_intent` is a complete upi:// string -- encode it as a QR in the app rather than shipping an image, and the code can never show a VPA the admin has since changed. The same string also deep-links straight into GPay or PhonePe.
Raise an add-fund request after paying through UPI. Send it as multipart/form-data when a screenshot is attached. Read `status` on the way back rather than assuming: normally it is `pending` and the balance does not move until an admin approves, but if auto-approval is switched on it comes back `approved` with the balance already updated.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
amount
number
Required
required, numeric, between the Minimum and Maximum Deposit settings
How much was paid in.
Example: 500
transaction_reference
string
Optional
nullable, max:100
The UTR or reference from the payment app.
Example: UPI2410166789
screenshot
file
Optional
nullable, image, jpg/jpeg/png/webp, max 5 MB
Proof of payment. Stored privately and only ever read by an admin.
Call this before showing the withdrawal form. If `is_open_now` is false, disable the button and show `days`, `opens_at` and `closes_at` instead of letting the request fail. `days` are ISO weekday numbers, 1 = Monday.
Raise a withdrawal. The amount leaves the wallet immediately and is held until an admin pays it; the balance in the response is already reduced. If the admin rejects it, the money comes back by itself and shows in the history as a withdrawal refund.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
amount
number
Required
required, numeric, at least the Minimum Withdrawal setting, within the daily cap
How much to withdraw.
Example: 2000
method
string
Required
required, in:bank,upi,phonepe,googlepay,paytm
How to be paid.
Example: phonepe
bank_account_id
integer
Sometimes
required when method is bank, must be one of the player's own saved accounts
The accounts a player can be paid into. Up to five; the first one saved becomes the primary automatically. Account numbers are always returned masked -- only the last four digits.
Remove an account. Refused while a pending withdrawal still points at it. If the primary one is removed, the next account becomes primary so the player is never left with accounts but no default.
Banners, notices and the information screens. All public, so the home screen can be drawn before anyone signs in. The text comes from Settings in the admin panel, so it changes without an app release.
Home Content
GET/api/v1/content
PublicRate limit: 120 / minute
Banners, the notices for one placement, and the support channels, in one call so the home screen does not have to make three.
Parameters
—
sent as query string
Parameter
Type
Required
Validation
Description
placement
string
Optional
nullable, in:home,app,add_fund,withdraw
Which screen the notices are for. Defaults to home.
The banner image itself. Use the `image_url` from the list rather than building this path by hand. Answers the image bytes, not JSON, and is cached for a day. A banner the admin has switched off answers 404.
Parameters
—
sent in the URL
Parameter
Type
Required
Validation
Description
slider
path
integer
Required
required, must be a live banner
Path parameter -- the banner id.
Example: 2
Responses
200The image bytes
"(binary image data)"
Possible error codes
NOT_FOUND
Information Page
GET/api/v1/content/pages/{page}
PublicRate limit: 120 / minute
The text for one information screen. Plain text, with blank lines between paragraphs. Edited in the admin panel under Settings, so it changes without an app release.
Parameters
—
sent in the URL
Parameter
Type
Required
Validation
Description
page
path
string
Required
required, one of: terms, how-to-play, about
Path parameter -- which screen.
Example: terms
Responses
200Success
{
"success": true,
"message": "Page fetched successfully.",
"data": {
"key": "terms",
"title": "Terms",
"body": "By using this app you agree ..."
}
}
Possible error codes
NOT_FOUNDMAINTENANCE_MODE
Contact Details
GET/api/v1/content/contact
PublicRate limit: 120 / minute
How players reach support. Any of these may be null -- hide the button when it is.
Parameters
None.
cURL
curl -X GET 'https://dpbossapp.io/api/v1/content/contact' \
-H 'Accept: application/json'
Messages a player sends from the contact screen, and the replies an admin writes back. A player only ever sees their own.
Send a Message
POST/api/v1/enquiries
Bearer token requiredRate limit: 5 / minute
Send support a message. Name and mobile fall back to the signed-in account, so normally only `message` needs sending.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
message
string
Required
required, min:5, max:2000
What the player wants to say.
Example: My withdrawal is still pending.
name
string
Optional
nullable, max:100
Defaults to the name on the account.
Example: Ramesh Patel
mobile
string
Optional
nullable, max:15
Defaults to the mobile on the account.
Example: 9876543210
Example request
{
"message": "My withdrawal is still pending.",
"name": "Ramesh Patel",
"mobile": "9876543210"
}
cURL
curl -X POST 'https://dpbossapp.io/api/v1/enquiries' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{"message":"My withdrawal is still pending.","name":"Ramesh Patel","mobile":"9876543210"}'
Responses
201Sent
{
"success": true,
"message": "Thanks -- we have your message and will reply soon.",
"data": {
"enquiry": {
"id": 9,
"message": "My withdrawal is still pending.",
"status": "open",
"admin_reply": null,
"replied_at": null,
"created_at": "2026-10-19T06:32:10+00:00"
}
}
}
Possible error codes
VALIDATION_ERRORTOO_MANY_ATTEMPTSUNAUTHENTICATED
My Messages
GET/api/v1/enquiries
Bearer token requiredRate limit: 120 / minute
The player's own messages, newest first. `status` moves from `open` to `replied` once an admin answers, and `admin_reply` carries what they wrote.
One-to-one chat with support. The messages never pass through this API: they live in Firestore, so they arrive on both sides without polling. All this API does is vouch for who the player is, by handing out a short-lived Firebase custom token. See FIREBASE-CHAT.md for the Firestore shape and the security rules.
Chat Availability
GET/api/v1/chat/config
Bearer token requiredRate limit: 120 / minute
Whether to show the chat screen at all. Check this before asking for a token, so a switched-off chat is a hidden button rather than a failed request.
{
"success": true,
"message": "Chat configuration fetched successfully.",
"data": {
"enabled": true,
"welcome_message": "Hi! How can we help you today?"
}
}
Possible error codes
UNAUTHENTICATED
Chat Token
POST/api/v1/chat/token
Bearer token requiredRate limit: 30 / minute
A Firebase custom token to sign in with, plus the Firestore paths this player may use. Sign in with signInWithCustomToken(token), then listen to messages_path ordered by created_at. The token lasts an hour -- ask for another when Firebase reports the session has gone, rather than holding one indefinitely.
{
"success": false,
"message": "Live chat is currently switched off.",
"error_code": "CHAT_DISABLED"
}
Notes
The uid is the player's account id, and the thread is keyed by the same id. The security rules match one against the other, so a player can only ever reach their own conversation.
Writing a message means two writes: the message itself, and the thread summary (last_message, last_message_at, unread_for_admin). Without the second, the admin list will not show it.
created_at must be serverTimestamp(). The rules reject anything else, so neither side can backdate a message.
Messages cannot be edited or deleted by either side.
Account creation. One step or two, depending on the OTP switch.
Register
POST/api/v1/auth/register
PublicRate limit: 10 / minute per IP
Starts registration. Validation runs the same way in both modes, so a duplicate mobile number or a weak password is rejected before any SMS is sent.
OTP ON
Nothing is written to the users table yet. An OTP is sent and the submitted data is held, encrypted, against the returned verification_id. Call /auth/register/verify-otp to finish.
OTP OFF
The account is created immediately and the response contains the access token. otp_required is false and no verification_id is returned.
PublicRate limit: 10 / minute per IP, plus the configured cooldown per verification
Issues a brand-new code for an existing verification. Works for both the registration and forgot-password flows — the purpose is taken from the verification_id. The wrong-attempt counter resets and the expiry window restarts.
{
"success": true,
"message": "A new OTP has been sent to your mobile number.",
"data": {
"verification_id": "9f2c1a84-7b3e-4d51-9c8a-1e2f3a4b5c6d",
"mobile": "9876543210",
"purpose": "registration",
"otp_length": 6,
"expires_at": "2026-10-04T12:50:00+00:00",
"expires_in_seconds": 600,
"resend_available_in_seconds": 60,
"resend_attempts_left": 2
}
}
429Too soon
{
"success": false,
"message": "Please wait 43 second(s) before requesting another OTP.",
"error_code": "OTP_COOLDOWN",
"retry_after": 43
}
Notes
Drive the resend button countdown from data.resend_available_in_seconds, and from retry_after on a 429.
When resend_attempts_left hits 0 the user must restart the flow.
{
"success": true,
"message": "Logged out from all devices."
}
Possible error codes
UNAUTHENTICATEDACCOUNT_INACTIVE
Password Reset
OTP-only recovery. Entirely gated by the admin OTP switch — check /app-config first.
Forgot Password Availability
GET/api/v1/auth/forgot-password/availability
PublicRate limit: 60 / minute
A tiny endpoint for deciding whether to render the “Forgot password?” link, if you would rather not cache the whole /app-config payload. Always answers 200.
Parameters
None.
cURL
curl -X GET 'https://dpbossapp.io/api/v1/auth/forgot-password/availability' \
-H 'Accept: application/json'
{
"success": true,
"message": "An OTP has been sent to your registered mobile number.",
"data": {
"otp_required": true,
"verification_id": "4c7d2e91-8a5b-4f63-b2d1-7e8f9a0b1c2d",
"mobile": "9876543210",
"purpose": "forgot_password",
"otp_length": 6,
"expires_at": "2026-10-04T13:10:00+00:00",
"expires_in_seconds": 600,
"resend_available_in_seconds": 60,
"resend_attempts_left": 3
}
}
403OTP switched off
{
"success": false,
"message": "Password reset is currently unavailable. Please contact support for help.",
"error_code": "OTP_DISABLED"
}
404Not registered
{
"success": false,
"message": "No account is registered with this mobile number.",
"error_code": "USER_NOT_FOUND"
}
Notes
This endpoint does confirm whether a number is registered, which the OTP flow unavoidably reveals. It is rate limited to 10 requests per minute per IP to blunt enumeration.
{
"success": true,
"message": "OTP verified. Use the reset_token to set a new password.",
"data": {
"verified": true,
"reset_token": "KxP9mQ2rT5vY8zA1bC4dE7fG0hJ3kL6nO9pR2sU5wX8yZ1aB4cD7eF0gH3iJ6kM9",
"reset_token_expires_in_seconds": 900
}
}
Notes
reset_token is always 64 characters. Keep it in memory only — do not persist it.
Step 3 of 3. Consumes the reset_token, sets the new password and revokes every existing token for the account, so all devices are signed out. The user then logs in again.
For a signed-in user who knows their current password. Available even when OTP is switched off. Other devices are signed out; the calling token keeps working.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
current_password
string
Required
required, must match the stored password
Verified against the authenticated account.
Example: Secret@123
password
string
Required
required, min:8, confirmed, different from current_password
Updates name and/or email. Both fields use `sometimes`, so send only what changed. The mobile number cannot be changed here — it is the account identity.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
name
string
Sometimes
sometimes required, min:2, max:100, letters / spaces / . ' - only
Call this from Firebase's onNewToken / onTokenRefresh callback. Tokens are keyed by device_id, so one handset keeps one record. If the same token turns up on another account it is detached from the old one, which handles a device changing hands.
Parameters
—
JSON body
Parameter
Type
Required
Validation
Description
fcm_token
string
Required
required, max:500
The fresh registration token.
Example: fZx9K2...A1b
device_type
string
Optional
nullable, in:android,ios,web
Platform.
Example: android
device_id
string
Optional
nullable, max:150
Strongly recommended — without it every refresh creates a new device row.
Example: a1b2c3d4e5f6
device_name
string
Optional
nullable, max:100
Device label.
Example: Redmi Note 13
app_version
string
Optional
nullable, max:20
App version.
Example: 1.0.1
notifications_enabled
boolean
Optional
nullable, boolean
Set false when the user turns notifications off in-app; the server then skips this device.
Soft-deletes the account, revokes all tokens and removes every device record. An admin can still see and restore it, and the mobile number stays reserved.