Back to list
OpenAEC-Foundation

erpnext-impl-whitelisted

by OpenAEC-Foundation

28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.

1🍴 1📅 Jan 23, 2026

SKILL.md


name: erpnext-impl-whitelisted description: "Implementation workflows and decision trees for Frappe Whitelisted Methods (REST APIs). Use when determining HOW to implement API endpoints: public vs authenticated, permission patterns, error handling, response formats, client integration. Triggers: how do I create API, build REST endpoint, frappe.call pattern, API permission check, guest API, secure endpoint."

ERPNext Whitelisted Methods - Implementation

This skill helps you determine HOW to implement REST API endpoints. For exact syntax, see erpnext-syntax-whitelisted.

Version: v14/v15/v16 compatible

Main Decision: What Type of API?

┌───────────────────────────────────────────────────────────────────┐
│ WHAT ARE YOU BUILDING?                                            │
├───────────────────────────────────────────────────────────────────┤
│                                                                   │
│ ► Public API (contact forms, status checks)?                      │
│   └── allow_guest=True + strict input validation                  │
│                                                                   │
│ ► Internal API for logged-in users?                               │
│   └── Default (no allow_guest) + permission checks                │
│                                                                   │
│ ► Admin-only API?                                                 │
│   └── frappe.only_for("System Manager")                           │
│                                                                   │
│ ► Document-specific method (on a form)?                           │
│   └── Controller method + frm.call()                              │
│                                                                   │
│ ► Standalone utility API?                                         │
│   └── Separate api.py + frappe.call()                             │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

→ See references/decision-tree.md for complete guide.


Decision: Where to Put API Code?

WHERE SHOULD YOUR API LIVE?
│
├─► Related to a specific DocType?
│   │
│   ├─► Called from that DocType's form?
│   │   └─► Controller method (doctype/xxx/xxx.py)
│   │       Client: frm.call('method_name', args)
│   │
│   └─► Standalone but DocType-related?
│       └─► Same file or doctype/xxx/xxx_api.py
│           Client: frappe.call('path.to.method', args)
│
├─► General utility API?
│   └─► myapp/api.py or myapp/api/module.py
│       Client: frappe.call('myapp.api.method', args)
│
└─► External integration?
    └─► myapp/integrations/service_name.py
        Often combined with webhooks

Decision: Permission Model

WHO CAN ACCESS THIS API?
│
├─► Anyone (public)?
│   └─► allow_guest=True
│       ⚠️ MUST validate all input
│       ⚠️ MUST rate limit if possible
│       ⚠️ NEVER expose sensitive data
│
├─► Any logged-in user?
│   └─► Default (no allow_guest)
│       Still check document permissions!
│
├─► Specific role(s)?
│   └─► frappe.only_for("Role") or frappe.only_for(["Role1", "Role2"])
│       Throws PermissionError if user lacks role
│
├─► Document-level permission?
│   └─► frappe.has_permission(doctype, ptype, doc)
│       Check before accessing each document
│
└─► Custom permission logic?
    └─► Implement your own checks
        Always deny by default

Quick Implementation Patterns

Pattern 1: Simple Authenticated API

# myapp/api.py
import frappe
from frappe import _

@frappe.whitelist()
def get_customer_balance(customer):
    """Get customer's outstanding balance."""
    # Permission check
    if not frappe.has_permission("Customer", "read", customer):
        frappe.throw(_("Not permitted"), frappe.PermissionError)
    
    # Fetch data
    balance = frappe.db.get_value("Customer", customer, "outstanding_amount")
    
    return {"customer": customer, "balance": balance or 0}
// Client call
frappe.call({
    method: 'myapp.api.get_customer_balance',
    args: { customer: 'CUST-00001' }
}).then(r => {
    console.log(r.message.balance);
});

Pattern 2: Public API with Validation

@frappe.whitelist(allow_guest=True, methods=["POST"])
def submit_inquiry(name, email, message):
    """Public contact form - strict validation required."""
    # Validate required fields
    if not all([name, email, message]):
        frappe.throw(_("All fields are required"))
    
    # Validate email format
    if not frappe.utils.validate_email_address(email):
        frappe.throw(_("Invalid email address"))
    
    # Sanitize input
    name = frappe.utils.strip_html(name)[:100]
    message = frappe.utils.strip_html(message)[:2000]
    
    # Create record
    doc = frappe.get_doc({
        "doctype": "Lead",
        "lead_name": name,
        "email_id": email,
        "notes": message,
        "source": "Website"
    })
    doc.insert(ignore_permissions=True)
    
    return {"success": True, "id": doc.name}

Pattern 3: Role-Restricted API

@frappe.whitelist()
def get_salary_data(employee):
    """HR-only endpoint."""
    # Role check - throws if not HR
    frappe.only_for(["HR Manager", "HR User"])
    
    return frappe.get_doc("Employee", employee).as_dict()

Pattern 4: Document Controller Method

# In doctype/sales_order/sales_order.py
class SalesOrder(Document):
    @frappe.whitelist()
    def calculate_shipping(self, carrier):
        """Called via frm.call() from form."""
        # Permission already checked by Frappe for doc access
        rate = get_shipping_rate(self.shipping_address, carrier)
        return {"carrier": carrier, "rate": rate}
// Client (in sales_order.js)
frm.call('calculate_shipping', {
    carrier: 'FedEx'
}).then(r => {
    frm.set_value('shipping_amount', r.message.rate);
});

→ See references/workflows.md for 10+ complete workflows.


Critical Security Rules

1. ALWAYS Check Permissions

# ❌ WRONG - exposes all data
@frappe.whitelist()
def get_document(doctype, name):
    return frappe.get_doc(doctype, name).as_dict()

# ✅ CORRECT
@frappe.whitelist()
def get_document(doctype, name):
    if not frappe.has_permission(doctype, "read", name):
        frappe.throw(_("Not permitted"), frappe.PermissionError)
    return frappe.get_doc(doctype, name).as_dict()

2. NEVER Trust User Input in SQL

# ❌ WRONG - SQL injection!
@frappe.whitelist()
def search(term):
    return frappe.db.sql(f"SELECT * FROM tabItem WHERE name LIKE '%{term}%'")

# ✅ CORRECT - parameterized
@frappe.whitelist()
def search(term):
    return frappe.db.sql("""
        SELECT name, item_name FROM tabItem 
        WHERE name LIKE %(term)s
        LIMIT 20
    """, {"term": f"%{term}%"}, as_dict=True)

3. VALIDATE All Input for Guest APIs

@frappe.whitelist(allow_guest=True)
def public_api(data):
    # ❌ WRONG - trusts input
    doc = frappe.get_doc(data)
    doc.insert(ignore_permissions=True)
    
    # ✅ CORRECT - validate everything
    if not isinstance(data, dict):
        frappe.throw(_("Invalid data format"))
    
    allowed_fields = {"name", "email", "message"}
    clean_data = {k: v for k, v in data.items() if k in allowed_fields}
    
    # Validate each field...

4. NEVER Expose Sensitive Data in Errors

# ❌ WRONG - leaks internal info
except Exception as e:
    frappe.throw(str(e))

# ✅ CORRECT - generic message, log details
except Exception:
    frappe.log_error(frappe.get_traceback(), "API Error")
    frappe.throw(_("An error occurred. Please try again."))

5. Use ignore_permissions Sparingly

# ❌ WRONG - bypasses all security
@frappe.whitelist()
def get_all_data():
    return frappe.get_all("Salary Slip", ignore_permissions=True)

# ✅ CORRECT - check role first
@frappe.whitelist()
def get_all_data():
    frappe.only_for("HR Manager")  # Verify role first!
    return frappe.get_all("Salary Slip", ignore_permissions=True)

Error Handling Pattern

Standard Error Response

@frappe.whitelist()
def robust_api(param):
    """API with proper error handling."""
    try:
        # Validate input
        if not param:
            frappe.throw(_("Parameter required"), frappe.ValidationError)
        
        # Check permissions
        if not frappe.has_permission("MyDocType", "read"):
            frappe.throw(_("Not permitted"), frappe.PermissionError)
        
        # Process
        result = do_something(param)
        return {"success": True, "data": result}
        
    except frappe.ValidationError:
        raise  # Let Frappe handle (417)
    except frappe.PermissionError:
        raise  # Let Frappe handle (403)
    except frappe.DoesNotExistError:
        frappe.local.response["http_status_code"] = 404
        return {"success": False, "error": "Not found"}
    except Exception:
        frappe.log_error(frappe.get_traceback(), "API Error")
        frappe.local.response["http_status_code"] = 500
        return {"success": False, "error": "Internal error"}

HTTP Status Codes

CodeExceptionWhen to Use
200-Success
201-Created (set manually)
400-Bad request (set manually)
401AuthenticationErrorNot logged in
403PermissionErrorAccess denied
404DoesNotExistErrorNot found
417ValidationErrorValidation failed
409DuplicateEntryErrorDuplicate
500ExceptionServer error

Response Patterns

Simple Return (Most Common)

@frappe.whitelist()
def get_data():
    return {"key": "value"}
# Response: {"message": {"key": "value"}}

List Response

@frappe.whitelist()
def get_items():
    return frappe.get_all("Item", fields=["name", "item_name"], limit=10)
# Response: {"message": [{"name": "...", "item_name": "..."}, ...]}

With Metadata

@frappe.whitelist()
def get_paged_data(page=1, page_size=20):
    offset = (int(page) - 1) * int(page_size)
    total = frappe.db.count("Item")
    items = frappe.get_all("Item", limit=page_size, start=offset)
    
    return {
        "data": items,
        "total": total,
        "page": page,
        "page_size": page_size,
        "pages": (total + page_size - 1) // page_size
    }

Client Integration

frappe.call() Options

frappe.call({
    method: 'myapp.api.my_method',
    args: { param1: 'value' },
    
    // UI Options
    freeze: true,                    // Show loading overlay
    freeze_message: __('Loading...'), // Custom message
    
    // Callbacks
    callback: function(r) {
        if (r.message) { /* success */ }
    },
    error: function(r) {
        // Handle error
    },
    always: function() {
        // Always runs (finally)
    },
    
    // Other
    async: true,                     // Default true
    type: 'POST'                     // Default POST
});

Async/Await Pattern

async function fetchData() {
    try {
        const r = await frappe.call({
            method: 'myapp.api.get_data',
            args: { id: 123 }
        });
        return r.message;
    } catch (e) {
        frappe.msgprint(__('Error loading data'));
        console.error(e);
    }
}

Reference Files

FileContents
decision-tree.mdComplete API type selection guide
workflows.mdStep-by-step implementation patterns
examples.mdComplete working examples
anti-patterns.mdCommon mistakes to avoid

Version Differences

Featurev14v15v16
@frappe.whitelist()
allow_guest
methods parameter
Type annotation validation
Rate limiting decorator
API v2 endpoints

v15+ Type Validation

# v15+ validates types automatically
@frappe.whitelist()
def typed_api(customer: str, limit: int = 10) -> dict:
    return {"customer": customer, "limit": limit}

v15+ Rate Limiting

from frappe.rate_limiter import rate_limit

@frappe.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60)  # 5 calls per minute
def rate_limited_api():
    return {"status": "ok"}

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon