Frappe_Claude_Skill_Package frappe-impl-whitelisted

install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/impl/frappe-impl-whitelisted" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-impl-whitelisted && rm -rf "$T"
manifest: skills/source/impl/frappe-impl-whitelisted/SKILL.md
source content

Frappe Whitelisted Methods Implementation Workflow

Step-by-step workflows for building API endpoints. For decorator syntax, see

frappe-syntax-whitelisted
.

Version: v14/v15/v16 (version-specific features noted)


Master Decision: What Type of Endpoint?

WHAT ARE YOU BUILDING?
│
├─► Public API (no login required)?
│   └─► allow_guest=True + STRICT input validation + rate limiting
│
├─► Authenticated API for logged-in users?
│   └─► Default @frappe.whitelist() + document permission checks
│
├─► Admin-only API?
│   └─► frappe.only_for("System Manager")
│
├─► Document-specific method (called from form)?
│   └─► Controller method + frm.call() from JS
│
├─► Standalone utility API?
│   └─► Separate api.py + frappe.call() from JS
│
├─► External webhook receiver?
│   └─► allow_guest=True + signature verification
│
└─► Background job trigger?
    └─► Authenticated API that calls frappe.enqueue()

Workflow 1: Design the Endpoint

Step 1: Choose Location

WHERE SHOULD THE CODE LIVE?
│
├─► Related to a DocType, called from its form?
│   └─► doctype/xxx/xxx.py (controller method)
│       Client: frm.call('method_name', args)
│
├─► Related to a DocType, standalone?
│   └─► doctype/xxx/xxx_api.py or myapp/api/module.py
│       Client: frappe.call('myapp.api.module.method')
│
├─► General app utility?
│   └─► myapp/api.py (small app) or myapp/api/module.py (large app)
│
└─► External integration?
    └─► myapp/integrations/service_name.py

Step 2: Choose Permission Model

WHO CAN CALL THIS API?
│
├─► Anyone (public) → allow_guest=True
│   ⚠️ MUST validate ALL input, sanitize for XSS, rate limit
│
├─► Any logged-in user → Default (no allow_guest)
│   Still check document permissions per record!
│
├─► Specific role(s) → frappe.only_for("Role")
│
└─► Document-level → frappe.has_permission(doctype, ptype, doc)

Step 3: Choose HTTP Method

WHAT DOES THE API DO?
│
├─► Read-only → methods=["GET"]
├─► Creates/modifies data → methods=["POST"]
└─► Both or default → omit methods parameter (all allowed)

Workflow 2: Implement an Authenticated API

Step-by-Step

Step 1: Create the function

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

@frappe.whitelist()
def get_customer_balance(customer):
    """Get outstanding balance for a customer."""
    # 1. Permission check
    if not frappe.has_permission("Customer", "read", customer):
        frappe.throw(_("Not permitted"), frappe.PermissionError)

    # 2. Validate input
    if not customer or not frappe.db.exists("Customer", customer):
        frappe.throw(_("Customer not found"), frappe.DoesNotExistError)

    # 3. Fetch and return
    balance = frappe.db.sql("""
        SELECT COALESCE(SUM(outstanding_amount), 0)
        FROM `tabSales Invoice`
        WHERE customer = %s AND docstatus = 1
    """, customer)[0][0]

    return {"customer": customer, "balance": balance}

Step 2: Call from Client Script

frappe.call({
    method: 'myapp.api.get_customer_balance',
    args: { customer: 'CUST-00001' },
    callback(r) {
        if (r.message) console.log(r.message.balance);
    }
});

Step 3: Test with curl

# Authenticate first
curl -X POST https://site.com/api/method/login \
  -d 'usr=admin&pwd=password'

# Call the API
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
  -H "Content-Type: application/json" \
  -d '{"customer": "CUST-00001"}' \
  --cookie cookies.txt

# Or use token auth
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
  -H "Authorization: token api_key:api_secret" \
  -H "Content-Type: application/json" \
  -d '{"customer": "CUST-00001"}'

Workflow 3: Implement a Public (Guest) API

Step-by-Step

Step 1: Create with strict validation

@frappe.whitelist(allow_guest=True, methods=["POST"])
def submit_inquiry(name, email, phone=None, message=None):
    """Public contact form — strict validation required."""
    # 1. Validate required fields
    if not all([name, email]):
        frappe.throw(_("Name and email are required"))

    # 2. Validate email format
    if not frappe.utils.validate_email_address(email):
        frappe.throw(_("Invalid email address"))

    # 3. Sanitize ALL input
    name = frappe.utils.strip_html(name)[:100]
    email = email.strip().lower()[:200]
    phone = frappe.utils.strip_html(phone)[:20] if phone else None
    message = frappe.utils.strip_html(message)[:2000] if message else None

    # 4. Create record with ignore_permissions
    lead = frappe.get_doc({
        "doctype": "Lead",
        "lead_name": name, "email_id": email,
        "phone": phone, "notes": message, "source": "Website"
    })
    lead.insert(ignore_permissions=True)

    return {"success": True, "message": _("Thank you")}

Step 2: Add rate limiting (v15+)

from frappe.rate_limiter import rate_limit

@frappe.whitelist(allow_guest=True, methods=["POST"])
@rate_limit(limit=5, seconds=60)  # 5 calls per minute
def submit_inquiry(name, email, phone=None, message=None):
    ...

Critical Rules for Guest APIs

  • ALWAYS validate and sanitize every input parameter
  • ALWAYS use
    methods=["POST"]
    for data-writing endpoints
  • ALWAYS add rate limiting (v15+ decorator or manual cache-based throttle on v14)
  • NEVER expose internal error details — log with
    frappe.log_error()
    , show generic message
  • NEVER return sensitive data (internal IDs, file paths, stack traces)
  • NEVER pass raw user input to
    frappe.get_doc()
    — use explicit field mapping

Workflow 4: Implement a Controller Method

Step-by-Step

Step 1: Add method to DocType controller

# myapp/doctype/sales_order/sales_order.py
class SalesOrder(Document):
    @frappe.whitelist()
    def calculate_shipping(self, carrier):
        """Called from form via frm.call()."""
        if not self.shipping_address:
            frappe.throw(_("Shipping address required"))
        rate = get_shipping_rate(self.shipping_address, carrier)
        return {"carrier": carrier, "rate": rate}

Step 2: Call from form JS

frm.call('calculate_shipping', {
    carrier: 'FedEx'
}).then(r => {
    frm.set_value('shipping_amount', r.message.rate);
});

Key difference: Frappe automatically checks document permissions for controller methods called via

frm.call()
. No manual permission check needed for the document itself.


Workflow 5: Implement Error Handling

Standard Pattern

@frappe.whitelist()
def process_payment(invoice, amount):
    try:
        # Validate
        if not invoice:
            frappe.throw(_("Invoice required"), frappe.ValidationError)
        if not frappe.has_permission("Sales Invoice", "write", invoice):
            frappe.throw(_("Not permitted"), frappe.PermissionError)

        # Process
        result = do_payment(invoice, float(amount))
        return {"success": True, "data": result}

    except (frappe.ValidationError, frappe.PermissionError):
        raise  # Let Frappe handle (417 / 403)
    except frappe.DoesNotExistError:
        frappe.throw(_("Not found"), frappe.DoesNotExistError)  # 404
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Payment Error")
        frappe.local.response["http_status_code"] = 500
        return {"success": False, "error": _("Internal error")}

HTTP Status Code Reference

CodeFrappe ExceptionWhen
200Success
403PermissionErrorAccess denied
404DoesNotExistErrorNot found
409DuplicateEntryErrorDuplicate
417ValidationErrorValidation failed
429Rate limit exceeded (v15+)
500ExceptionServer error

Workflow 6: File Upload Endpoint

@frappe.whitelist()
def upload_attachment(doctype, docname):
    """Handle file upload attached to a document."""
    if not frappe.has_permission(doctype, "write", docname):
        frappe.throw(_("Not permitted"), frappe.PermissionError)

    file = frappe.request.files.get('file')
    if not file:
        frappe.throw(_("No file provided"))

    file_doc = frappe.get_doc({
        "doctype": "File",
        "file_name": file.filename,
        "attached_to_doctype": doctype,
        "attached_to_name": docname,
        "content": file.read(),
        "is_private": 1
    })
    file_doc.insert(ignore_permissions=True)
    return {"file_url": file_doc.file_url}

Workflow 7: Background Job Endpoint

@frappe.whitelist()
def start_heavy_export(doctype, filters=None):
    """Trigger a background job — returns immediately."""
    frappe.only_for("System Manager")

    frappe.enqueue(
        "myapp.tasks.export_data",
        queue="long",
        timeout=1500,
        doctype=doctype,
        filters=filters,
        user=frappe.session.user
    )
    return {"status": "queued", "message": _("Export started")}

Client Integration Patterns

frappe.call() with Options

frappe.call({
    method: 'myapp.api.my_method',
    args: { param: 'value' },
    freeze: true,
    freeze_message: __('Processing...'),
    callback(r) { console.log(r.message); },
    error(r) { frappe.msgprint(__('Error')); }
});

Async/Await Pattern

const r = await frappe.call({
    method: 'myapp.api.get_data',
    args: { id: 123 }
});
console.log(r.message);

REST API from External System

# Token auth (recommended for integrations)
curl -H "Authorization: token api_key:api_secret" \
  https://site.com/api/method/myapp.api.method

# Bearer auth (OAuth)
curl -H "Authorization: Bearer access_token" \
  https://site.com/api/method/myapp.api.method

Security Rules (ALWAYS/NEVER)

  1. ALWAYS check permissions before accessing any document
  2. ALWAYS use parameterized queries — NEVER use f-strings in SQL
  3. ALWAYS validate and sanitize input for guest APIs
  4. ALWAYS check role with
    frappe.only_for()
    before using
    ignore_permissions=True
  5. NEVER expose internal errors — log details, return generic message
  6. NEVER use
    methods=["GET"]
    for endpoints that modify data
  7. NEVER trust
    data
    dicts from guest APIs — use explicit parameter names
  8. ALWAYS include
    X-Frappe-CSRF-Token
    header in fetch() calls from browser

Migration: Server Script API to Whitelisted Method

StepAction
1Copy Server Script logic to
myapp/api/module.py
2Add
@frappe.whitelist()
decorator with same permission model
3Update all
frappe.call()
references to new dotted path
4Disable or delete the Server Script
5Run
bench --site sitename migrate

Version Differences

Featurev14v15v16
@frappe.whitelist()YesYesYes
allow_guestYesYesYes
methods parameterYesYesYes
Type annotation validationNoYesYes
@rate_limit decoratorNoYesYes
API v2 endpointsNoYesYes

v15+ Type Validation

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

Reference Files

FileContents
decision-tree.mdComplete API type and permission selection guide
workflows.mdStep-by-step implementation patterns (10+ workflows)
examples.mdProduction-ready code examples