agentsclimarketplace

Frappe impl whitelisted

Skill Impertio-Studio/Frappe_Claude_Skill_Package/skills/source/impl/frappe-impl-whitelisted

60 deterministic Claude AI skills for Frappe Framework & ERPNext v14-v16 development and operations

Install
npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-whitelisted

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

What its author says it does

Copied from the file, not written here

Use when building API endpoints with @frappe.whitelist() in Frappe. Covers endpoint design, permission patterns, error handling, client integration, file uploads, background jobs, rate limiting, REST API testing, and migration from Server Scripts to whitelisted methods. Prevents permission bypasses, SQL injection, and data exposure. Keywords: how to create API, build REST endpoint, frappe.call,, create API endpoint, call from frontend, custom API, REST endpoint, how to call python from JS. frappe.whitelist, API permission, guest API, secure endpoint, rate limiting, curl testing, frm.call.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

12.5 KB, as published. Nobody here has run it

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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.