Mobile Control - Custom Frappe Application
You can install this app using the bench CLI:
cd $PATH_TO_YOUR_BENCH
bench get-app $URL_OF_THIS_REPO --branch develop
bench install-app mobile_controlThese endpoints are exposed as whitelisted methods and are intended for a mobile
client. All endpoints expect POST requests and use the api/v2/method/ route.
Endpoints:
mobile_auth.login- Login with username/password.mobile_auth.logout- Logout and revoke refresh tokens.mobile_auth.send_login_otp- Send OTP to mobile number for login.mobile_auth.verify_login_otp- Verify OTP and complete login.mobile_auth.refresh_token- Refresh access token using refresh token.mobile_auth.permissions- Get current user permissions (requires authentication).mobile_auth.get_translations- Get translation dictionary for one or more languages (requires authentication). By default returns DB translations only; useall=1for full (apps + DB). Uselang=hi,enfor multiple languages.mobile_auth.get_social_login_providers- Discover enabled social providers fromSocial Login Key(guest).mobile_auth.get_social_authorize_url- Build provider-direct OAuth authorize URL for one-tap social login (guest).
Response tokens:
access_tokenexpires in 24 hours.refresh_tokenexpires in 30 days and is rotated on every refresh.
These APIs are designed for mobile SDK OAuth (PKCE) login and avoid the extra provider selection click on Frappe login page.
- Providers are auto-discovered from Frappe
Social Login Key. - No extra provider config is required in
mobile_control. - Sensitive data such as
client_secretis never returned.
Required mobile callback URI:
frappemobilesdk://oauth/callback
Provider discovery:
GET /api/method/mobile_auth.get_social_login_providers
Example response:
{
"providers": [
{ "id": "google", "label": "Google", "icon_url": "https://..." },
{ "id": "microsoft", "label": "Microsoft", "icon_url": null }
]
}Provider-direct authorize URL:
POST /api/v2/method/mobile_auth.get_social_authorize_url
Body:
{
"provider": "google",
"client_id": "mobile-client-id",
"redirect_uri": "frappemobilesdk://oauth/callback",
"scope": "openid all",
"state": "random-state",
"code_challenge": "pkce-code-challenge",
"code_challenge_method": "S256"
}Example response:
{
"authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?...state=<encoded-with-redirect_to>"
}Notes:
- Returned URL is generated by Frappe Social Login flow for the selected provider.
client_idin request body is your Frappe OAuth Client ID (not Google client id).- After social login succeeds, Frappe redirects to its OAuth authorize endpoint, which then redirects to
redirect_uri(frappemobilesdk://oauth/callback) with authorization code.
Troubleshooting:
- Provider not showing: verify
Social Login Keyexists and social login is enabled for that key. - Invalid provider error: ensure
providermatches discovered providerid(google,microsoft, etc). - Redirect URI mismatch: ensure app sends
frappemobilesdk://oauth/callback(or add allowlisted URIs in site config). - Social opens wrong provider or fails immediately: verify
Social Login Keyfor that provider is enabled and configured with valid client credentials.
Login, mobile_auth.verify_login_otp, and mobile_auth.refresh_token return a response like:
{
"message": "Logged In",
"user": "user@example.com",
"full_name": "User Name",
"language": "en",
"access_token": "...",
"refresh_token": "...",
"offline_enabled": false,
"mobile_form_names": [
{
"mobile_workspace_item": "Mobile Refresh Token",
"group_name": "",
"doctype_meta_modified_at": "2026-02-14 14:40:49.962439",
"doctype_icon": ""
}
],
"roles": ["Mobile User", "All", "Desk User"],
"permissions": [
{
"doctype": "Mobile Refresh Token",
"read": true,
"write": false,
"create": true,
"delete": false,
"submit": false,
"cancel": false,
"amend": false
}
]
}languageis the user's language (default"en"if blank).offline_enabledis the value of theMobile Configuration.offline_enabledCheck field. The mobile SDK uses this to decide whether to run as an offline-first client or a thin online client. Defaultfalse. Only emitted when the parentenabledflag is on. See the SDK'sdoc/OFFLINE_MODE_TOGGLE.mdfor the full client-side contract.rolesis an array of role names.permissionsis an array of objects; each hasdoctypeand the flagsread,write,create,delete,submit,cancel,amend.
Client flow:
- Login or OTP verify to receive
access_token+refresh_token. - Use
access_tokenasAuthorization: Bearer <access_token>for API calls. - When access token expires, call
mobile_auth.refresh_tokenwith therefresh_tokento get a new pair.
User permissions:
- Permissions are automatically included in login, OTP verify, and refresh token responses.
- Permissions include user roles and doctype-level permissions (read, write, create, delete, submit, cancel, amend) for mobile-configured doctypes.
- To refresh permissions without re-authenticating, call
mobile_auth.permissionsendpoint.
Translations:
- Call
GET /api/method/mobile_auth.get_translations?lang=hi(Bearer token required). Omitlangfor English (en). By default only DB translations (Translation doctype) are returned. Add&all=1or&all=trueto get full translations (apps CSV/MO + DB). For multiple languages, use comma-separated codes:?lang=hi,en. Response is always the same shape:{ "langs": ["hi"], "translations_by_lang": { "hi": { "source text": "translated text", ... } } }(or multiple keys inlangsandtranslations_by_lang). Usetranslations_by_lang[lang][source] ?? sourcefor lookup.
All requests use:
POST {{base_url}}/api/v2/method/<endpoint>
Login:
{
"username": "your.username",
"password": "your.password"
}Send OTP:
{
"mobile_no": "+15551234567"
}Verify OTP:
{
"tmp_id": "TMP_ID_FROM_SEND_OTP",
"otp": "123456"
}Refresh token:
{
"refresh_token": "REFRESH_TOKEN"
}Logout:
Authorization: Bearer <access_token>
Get permissions:
GET {{base_url}}/api/method/mobile_auth.permissions
Authorization: Bearer <access_token>
Response:
{
"roles": ["Mobile User", "System Manager"],
"permissions": [
{
"doctype": "Customer",
"read": true,
"write": true,
"create": true,
"delete": false,
"submit": true,
"cancel": false,
"amend": false
}
]
}The API/ directory contains a Bruno collection to try the mobile auth endpoints locally.
Collection: API/bruno.json
Requests:
| File | Description |
|---|---|
Login with username and password.bru |
POST login with username/password; returns access_token, refresh_token, roles, permissions, language. |
Login with mobile.bru |
POST send OTP to mobile number (mobile_auth.send_login_otp). |
Login with mobile verify.bru |
POST verify OTP and login (mobile_auth.verify_login_otp). |
Get Access Token.bru |
POST refresh token to get new access_token and refresh_token. |
Logout.bru |
POST logout (Bearer token required); revokes refresh tokens. |
permissions.bru |
GET current user roles and permissions (Bearer token required). |
get_translations.bru |
GET translation dictionary; optional ?lang=hi (Bearer token required). |
App Status.bru |
GET app status (enabled, package_name, app_title, version). Guest. |
App Configuration.bru |
GET mobile configuration list. Guest. |
Setup: Set base_url in collection/environment variables. For auth requests, set username, password, and after login use the returned access_token as Bearer token in subsequent requests (or use Bruno’s response scripts to save the token).
When a mobile push permanently fails on the server (a terminal 4XX, terminal 401, or 5XX), the SDK records the exact failing request and posts an aggregated report here. The goal is to let a developer reconstruct a replayable request — same payload, same user, same permissions — without asking the field user for steps to reproduce. Capture is automatic on the device; the host app does not wire anything up.
Two doctypes back the feature:
| DocType | Purpose |
|---|---|
| Mobile Error Log | One row per unique failure signature. Holds the failing DocType, operation, HTTP status, exception type, request method/URL, the reporting user (raw value + User link), their roles, trace id, app version, device, an occurrence_count, and last_seen. Read-only; System Manager read/delete only. |
| Mobile Error Log Example (child) | A rolling window of the 5 most recent occurrences for a signature. Each holds the mobile_uuid, occurred_at, the JSON request_payload, the server response_body, and a rendered curl_command. |
POST (whitelisted). Resolves to mobile_control.api.error_log.report_error. Takes a single payload argument — the JSON object the SDK's ErrorLogCollector aggregates per drain:
{
"signature": "<client-computed stable hash of the failure>",
"doctype_name": "Household Survey",
"operation": "update",
"http_status": 417,
"exc_type": "...",
"request_method": "PUT",
"request_url": "https://.../api/resource/Household%20Survey/...",
"error_user": "Administrator",
"error_user_roles": ["System Manager", "..."],
"trace_id": "<X-Frappe-Request-Id, if server monitoring is on>",
"app_version": "...",
"device": "...",
"occurrence_count": 1,
"examples": [
{
"mobile_uuid": "...",
"occurred_at": "2026-06-12 12:00:00",
"request_payload": "<verbatim request body JSON>",
"response_body": "<verbatim server response>"
}
]
}Behaviour:
- UPSERT by
signature— the signature is taken verbatim and never recomputed server-side. A repeat failure incrementsoccurrence_countand refreshes the volatile fields (roles, trace id,last_seen) rather than creating a new row. - Last-5 examples — incoming examples are appended and evicted to the most recent 5 per signature.
signatureis required; a missing/blank one is aValidationError.- Saved with
ignore_links(a deactivated or renamederror_usermust never block the insert) andignore_permissions(the doctype has no create permission by design).
Returns {"status": "ok", "name": "<row name>"}.
Each example stores a curl_command rendered from its method, URL, and payload. The host and auth token are never stored — they are emitted as {{HOST}} and {{TOKEN}} placeholders. To replay: substitute your own bench host and a token for the same user (error_user), then run the CURL to reproduce the original server response.
Rows are purged daily by mobile_control.tasks.purge_mobile_error_logs. Retention is configurable on Mobile Configuration via mobile_error_log_retention_days (default 30); rows older than that are deleted.
This app uses pre-commit for code formatting and linting. Please install pre-commit and enable it for this repository:
cd apps/mobile_control
pre-commit installPre-commit is configured to use the following tools for checking and formatting your code:
- ruff
- eslint
- prettier
- pyupgrade
