Skip to content
erpnext-impl-whitelisted logo

ERPNext Whitelisted Methods - Implementation

erpnext-impl-whitelisted

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, frap...

SKILL.md

Full skill instructions

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

More Security skills

supabase logo
Security

supabase

Handles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...

2.7K 308.9K
View

Comprehensive guides and best practices for Neon Serverless Postgres, covering setup, connection methods, authentication, and platform APIs.

98 216.2K
View

Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules.

462 163.8K
View

A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.

462 127.7K
View

Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.

462 125.1K
View

Complete Better Auth server and client setup with database adapters, session management, plugins, and security configuration.

222 118.2K
View

Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".

31.9K 116.1K
View
cloudflare logo
Security

cloudflare

Complete Cloudflare platform integration with decision trees for compute, storage, AI, networking, security, and infrastructure-as-code.

3K 110.7K
View

Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.

253 103K
View
gws-gmail logo
Security

gws-gmail

Send, read, and manage Gmail messages, drafts, labels, and account settings.

31.2K 78.9K
View

Comprehensive website auditing across 230+ rules in 21 categories including SEO, performance, security, and accessibility.

94 72.3K
View
gws-shared logo
Security

gws-shared

Shared authentication, CLI syntax, and output formatting patterns for gws Google Workspace commands.

31.2K 63.4K
View

Security AI tools

StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
GeoSpy logo
Security

GeoSpy

GeoSpy: Pricing, Features, FAQs, and Alternatives for AI Teams

Free
View

Detect AI-generated voices to protect against audio fraud.

Paid
View

Ensure Your Content's Originality with AI Plagiarism Checker

Freemium
View
Img Upscaler logo
Security

Img Upscaler

Upscale images by 400% without quality loss

Freemium
View
AICheatCheck logo
Security

AICheatCheck

Accurately Detect AI-Generated Content with TheChecker.AI

Free
View
D
Security

Detect GPT

Chrome extension that detects and flags AI-generated content

Free
View