erpnext-impl-whitelisted

ERPNext Whitelisted Methods - Implementation

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-impl-whitelisted" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-erpnext-impl-whitelisted

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

  1. 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)

  1. 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."))

  1. 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

Code Exception When to Use

200

Success

201

Created (set manually)

400

Bad request (set manually)

401 AuthenticationError Not logged in

403 PermissionError Access denied

404 DoesNotExistError Not found

417 ValidationError Validation failed

409 DuplicateEntryError Duplicate

500 Exception Server 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

File Contents

decision-tree.md Complete API type selection guide

workflows.md Step-by-step implementation patterns

examples.md Complete working examples

anti-patterns.md Common mistakes to avoid

Version Differences

Feature v14 v15 v16

@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"}

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-customapp

No summary provided by upstream source.

Repository SourceNeeds Review