Claude-skill-registry erpnext-syntax-whitelisted
Deterministic syntax for Frappe Whitelisted Methods (Python API endpoints) for v14/v15/v16. Use when Claude needs to generate code for API functions, REST endpoints, @frappe.whitelist() decorator, frappe.call() or frm.call() invocations, permission checks in APIs, error handling patterns, or when questions concern API structure, response formats, or client-server communication. Triggers: whitelisted, API endpoint, frappe.call, frm.call, REST API, @frappe.whitelist, allow_guest, API method.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/erpnext-syntax-whitelisted" ~/.claude/skills/majiayu000-claude-skill-registry-erpnext-syntax-whitelisted && rm -rf "$T"
skills/data/erpnext-syntax-whitelisted/SKILL.mdERPNext 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 |
|---|---|---|
| | = accessible without login |
| All | , , or combination |
| | = 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 |
|---|---|---|
| 417 | Validation errors |
| 403 | Access denied |
| 404 | Not found |
| 409 | Duplicate |
| 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 (
orfrappe.has_permission()
)frappe.only_for() - Input validation (types, ranges, formats)
- No SQL injection (parameterized queries)
- No sensitive data in error messages
-
only with explicit reasonallow_guest=True -
only with role checkignore_permissions=True - 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 | ❌ | ✅ |
| Rate limiting decorators | ❌ | ✅ |
| Document method endpoint | N/A | |
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 |