erpnext-syntax-whitelisted
SKILL.md
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()orfrappe.only_for()) - Input validation (types, ranges, formats)
- No SQL injection (parameterized queries)
- No sensitive data in error messages
-
allow_guest=Trueonly with explicit reason -
ignore_permissions=Trueonly with role check - HTTP method restricted where possible
Critical Rules
1. 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=["*"])
2. 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)
3. 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 |
Weekly Installs
1
Repository
smithery/aiFirst Seen
Feb 5, 2026
Installed on
replit1
amp1
opencode1
kimi-cli1
codex1
github-copilot1