NYC
skills/smithery/ai/erpnext-syntax-whitelisted

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() 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

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/ai
First Seen
Feb 5, 2026
Installed on
replit1
amp1
opencode1
kimi-cli1
codex1
github-copilot1