ERPNext Syntax: Whitelisted Methods
Whitelisted Methods expose Python functions as REST API endpoints.
Quick Reference
Basic Whitelisted Method
import frappe
@frappe.whitelist() def get_customer_summary(customer): """Basic API endpoint - authenticated users only.""" if not frappe.has_permission("Customer", "read"): frappe.throw(_("Not permitted"), frappe.PermissionError)
return frappe.get_doc("Customer", customer).as_dict()
Endpoint URL
/api/method/myapp.api.get_customer_summary
Decorator Options
Parameter Default Description
allow_guest
False
True = accessible without login
methods
All ["GET"] , ["POST"] , or combination
xss_safe
False
True = don't escape HTML
Public endpoint, POST only
@frappe.whitelist(allow_guest=True, methods=["POST"]) def submit_contact_form(name, email, message): # Validate input carefully with guest access! if not name or not email: frappe.throw(_("Name and email required")) return {"success": True}
Read-only endpoint
@frappe.whitelist(methods=["GET"]) def get_status(order_id): return frappe.db.get_value("Sales Order", order_id, "status")
Full options: See decorator-options.md
Permission Patterns
ALWAYS Check Permissions
@frappe.whitelist() def get_data(doctype, name): # Check BEFORE fetching data if not frappe.has_permission(doctype, "read", name): frappe.throw(_("Not permitted"), frappe.PermissionError) return frappe.get_doc(doctype, name).as_dict()
Role-Based Access
@frappe.whitelist() def admin_function(): frappe.only_for("System Manager") # Throws if user lacks role return {"admin_data": "sensitive"}
@frappe.whitelist() def multi_role_function(): frappe.only_for(["System Manager", "HR Manager"]) return {"data": "value"}
Security patterns: See permission-patterns.md
Error Handling
frappe.throw() for User-Facing Errors
@frappe.whitelist() def process_order(order_id, amount): # Validation error if not order_id: frappe.throw(("Order ID required"), title=("Missing Data"))
# Permission error
if not frappe.has_permission("Sales Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
# Business logic error
if amount < 0:
frappe.throw(
_("Amount cannot be negative: {0}").format(amount),
frappe.ValidationError
)
Exception Types and HTTP Codes
Exception HTTP Code When
frappe.ValidationError
417 Validation errors
frappe.PermissionError
403 Access denied
frappe.DoesNotExistError
404 Not found
frappe.DuplicateEntryError
409 Duplicate
frappe.AuthenticationError
401 Not logged in
Robust Error Pattern
@frappe.whitelist() def robust_api(param): try: result = process_data(param) return {"success": True, "data": result} except frappe.DoesNotExistError: frappe.local.response["http_status_code"] = 404 return {"success": False, "error": "Not found"} except frappe.PermissionError: frappe.local.response["http_status_code"] = 403 return {"success": False, "error": "Access denied"} except Exception: frappe.log_error(frappe.get_traceback(), "API Error") frappe.local.response["http_status_code"] = 500 return {"success": False, "error": "Internal error"}
Full error patterns: See error-handling.md
Response Patterns
Return Value (Recommended)
@frappe.whitelist() def get_summary(customer): return { "customer": customer, "total": 15000 }
Response: {"message": {"customer": "...", "total": 15000}}
Custom HTTP Status
@frappe.whitelist() def create_item(data): if not data: frappe.local.response["http_status_code"] = 400 return {"error": "Data required"} # ... create item frappe.local.response["http_status_code"] = 201 return {"created": True}
Full response patterns: See response-patterns.md
Client Calls
frappe.call() - Standalone APIs
// Promise-based (recommended) frappe.call({ method: 'myapp.api.get_customer_summary', args: { customer: 'CUST-00001' } }).then(r => { console.log(r.message); });
// With loading indicator frappe.call({ method: 'myapp.api.process_data', args: { data: myData }, freeze: true, freeze_message: __('Processing...') });
frm.call() - Controller Methods
frm.call('calculate_taxes', { include_shipping: true }).then(r => { frm.set_value('tax_amount', r.message.tax_amount); });
Full client patterns: See client-calls.md
Decision Tree: Which Options?
Who may call the API? │ ├─► Anyone (including guests)? │ └─► allow_guest=True + extra input validation │ └─► Logged-in users only? │ └─► Specific role required? ├─► Yes → frappe.only_for("RoleName") in method └─► No → frappe.has_permission() check
Which HTTP methods? │ ├─► Read only? │ └─► methods=["GET"] │ ├─► Write only? │ └─► methods=["POST"] │ └─► Both? └─► methods=["GET", "POST"] or default (all)
Security Checklist
For EVERY whitelisted method:
-
Permission check present (frappe.has_permission() or frappe.only_for() )
-
Input validation (types, ranges, formats)
-
No SQL injection (parameterized queries)
-
No sensitive data in error messages
-
allow_guest=True only with explicit reason
-
ignore_permissions=True only with role check
-
HTTP method restricted where possible
Critical Rules
- NEVER Skip Permission Check
❌ WRONG - anyone can see all data
@frappe.whitelist() def get_all_salaries(): return frappe.get_all("Salary Slip", fields=["*"])
✅ CORRECT
@frappe.whitelist() def get_salaries(): frappe.only_for("HR Manager") return frappe.get_all("Salary Slip", fields=["*"])
- NEVER Use User Input in SQL
❌ WRONG - SQL injection!
@frappe.whitelist() def search(term): return frappe.db.sql(f"SELECT * FROM tabCustomer WHERE name LIKE '%{term}%'")
✅ CORRECT - parameterized
@frappe.whitelist() def search(term): return frappe.db.sql(""" SELECT * FROM tabCustomer WHERE name LIKE %(term)s """, {"term": f"%{term}%"}, as_dict=True)
- NEVER Leak Sensitive Data in Errors
❌ WRONG - leaks internal information
except Exception as e: frappe.throw(str(e)) # May leak stack traces!
✅ CORRECT
except Exception: frappe.log_error(frappe.get_traceback(), "API Error") frappe.throw(_("An error occurred"))
All anti-patterns: See anti-patterns.md
Version Differences (v14 vs v15)
Feature v14 v15
Type annotations validation ❌ ✅
API v2 endpoints ❌ ✅ /api/v2/
Rate limiting decorators ❌ ✅ @rate_limit()
Document method endpoint N/A /api/v2/document/{dt}/{name}/method/{m}
v15 Type Validation
@frappe.whitelist() def get_orders(customer: str, limit: int = 10) -> dict: """v15 validates types automatically on request.""" return {"orders": frappe.get_all("Sales Order", limit=limit)}
Reference Files
File Content
decorator-options.md All @frappe.whitelist() parameters
parameter-handling.md Request parameters and type conversion
response-patterns.md Response types and structures
client-calls.md frappe.call() and frm.call() patterns
permission-patterns.md Security best practices
error-handling.md Error patterns and exception types
examples.md Complete working API examples
anti-patterns.md What to avoid