Claude
Skills
Sign in
Back

frappe-api-handler

Included with Lifetime
$97 forever

Create custom API endpoints and whitelisted methods for Frappe applications. Use when building REST APIs or custom endpoints.

Backend & APIs

What this skill does


# Frappe API Handler Skill

Create secure, efficient custom API endpoints for Frappe applications.

## Global Rules

These Frappe conventions apply to everything this skill generates, and override any conflicting example below.

- **Bench commands:** use bare `bench` (never `./env/bin/bench` or a full path). Always pass `--site <site>` explicitly — never run a bare `bench migrate` / `bench run-tests`. Run `bench start` in the background and only if it isn't already running. Don't run discovery commands (`which bench`, `bench --version`).
- **DocType files** live at `apps/<app>/<app>/<module>/doctype/<name>/<name>.json` — the app name appears twice (directory + Python package) — with an empty `__init__.py` alongside. Never `mkdir` the folder; write the JSON and run `bench --site <site> migrate` to create the structure. Don't add `creation`, `modified`, `owner`, `modified_by`, or `docstatus` as fields — Frappe manages them.
- **Database & ORM:** prefer `frappe.qb.get_query()` over raw `frappe.db.sql()`. Use `frappe.db.get_all()` for server logic (ignores permissions) and `frappe.db.get_list()` for user-facing APIs (enforces them). Never use `frappe.db.set_value()` on a field with validation or lifecycle logic — load the doc and `doc.save()` so controller hooks run. Batch-fetch related records; never query inside a loop (N+1).
- **Never call `frappe.db.commit()`** in controllers, request handlers, background jobs, or patches — Frappe auto-commits on success and rolls back on uncaught errors. Flush manually only to make a write visible to a subsequent `frappe.enqueue()` (or pass `enqueue_after_commit=True`).
- **Permissions & APIs:** put permission checks inside controller methods (enforced on every call path), not in API wrappers. Type-hint every `@frappe.whitelist()` parameter so Frappe validates and casts it, and pass `methods=[...]` to pin the HTTP verb.

## When to Use This Skill

Claude should invoke this skill when:
- User wants to create custom API endpoints
- User needs to whitelist Python methods for API access
- User asks about REST API implementation
- User wants to integrate external systems with Frappe
- User needs help with API authentication or permissions

## Capabilities

### 1. Whitelisted Methods

Create Python methods accessible via API. Always type-hint parameters (Frappe validates and casts args by type hint, preventing type-confusion; without hints all args arrive as untrusted strings) and pin the HTTP verb with `methods=[...]`:

```python
import frappe
from frappe import _

@frappe.whitelist(methods=["GET"])
def get_customer_details(customer_name: str):
    """Get customer details with validation"""
    # Permission check (mirror the controller; keep checks on every call path)
    if not frappe.has_permission("Customer", "read"):
        frappe.throw(_("Not permitted"), frappe.PermissionError)

    customer = frappe.get_doc("Customer", customer_name)

    return {
        "name": customer.name,
        "customer_name": customer.customer_name,
        "email": customer.email_id,
        "phone": customer.mobile_no,
        "outstanding_amount": customer.get_outstanding()
    }
```

### 2. API Method Patterns

**Public Methods (No Authentication):**
```python
@frappe.whitelist(allow_guest=True, methods=["GET"])
def public_api_method():
    """Accessible without login. Return only fields guests need — never leak
    user emails, internal IDs, or permission-sensitive data."""
    return {"message": "Public data"}
```

**Authenticated Methods:**
```python
@frappe.whitelist(methods=["GET"])
def authenticated_method():
    """Requires valid session or API key"""
    user = frappe.session.user
    return {"user": user}
```

**Permission-based Methods:**
```python
@frappe.whitelist(methods=["POST"])
def delete_customer(customer_name: str):
    """Check permissions before action"""
    if not frappe.has_permission("Customer", "delete"):
        frappe.throw(_("Not permitted"))

    frappe.delete_doc("Customer", customer_name)
    return {"message": "Customer deleted"}
```

### 3. REST API Endpoints

**GET Request Handler:** (use `get_list` for user-facing reads so DocType permissions are enforced)
```python
@frappe.whitelist(methods=["GET"])
def get_items(filters: dict | None = None, fields: list | None = None, limit: int = 20):
    """Get list of items with filters"""
    items = frappe.get_list(
        "Item",
        filters=filters or {},
        fields=fields or ["name"],
        limit=limit,
        order_by="creation desc"
    )

    return {"items": items}
```

**POST Request Handler:**
```python
@frappe.whitelist(methods=["POST"])
def create_sales_order(customer: str, items: list, delivery_date: str | None = None):
    """Create sales order from API. No frappe.db.commit() — Frappe commits POST on success."""
    doc = frappe.get_doc({
        "doctype": "Sales Order",
        "customer": customer,
        "delivery_date": delivery_date or frappe.utils.today(),
        "items": items
    })

    doc.insert()
    doc.submit()

    return {"name": doc.name, "grand_total": doc.grand_total}
```

**PUT/UPDATE Handler:**
```python
@frappe.whitelist(methods=["PUT"])
def update_customer(customer_name: str, data: dict):
    """Update customer details"""
    doc = frappe.get_doc("Customer", customer_name)
    doc.update(data)
    doc.save()

    return {"name": doc.name, "message": "Updated successfully"}
```

**DELETE Handler:**
```python
@frappe.whitelist(methods=["DELETE"])
def delete_document(doctype: str, name: str):
    """Delete a document"""
    if not frappe.has_permission(doctype, "delete"):
        frappe.throw(_("Not permitted"))

    frappe.delete_doc(doctype, name)
    return {"message": f"{doctype} {name} deleted"}
```

### 4. Error Handling

```python
@frappe.whitelist(methods=["POST"])
def safe_api_method(param: str):
    """API method with proper error handling"""
    try:
        # Validate input
        if not param:
            frappe.throw(_("Parameter is required"))

        # Process request
        result = process_data(param)

        return {"success": True, "data": result}

    except frappe.ValidationError as e:
        frappe.log_error(frappe.get_traceback(), "API Validation Error")
        return {"success": False, "message": str(e)}

    except Exception as e:
        frappe.log_error(frappe.get_traceback(), "API Error")
        return {"success": False, "message": "Internal server error"}
```

### 5. Input Validation

```python
@frappe.whitelist(methods=["POST"])
def validated_method(email: str, phone: str, amount: float):
    """Validate all inputs"""
    # Email validation
    if not frappe.utils.validate_email_address(email):
        frappe.throw(_("Invalid email address"))

    # Phone validation
    if not phone or len(phone) < 10:
        frappe.throw(_("Invalid phone number"))

    # Amount validation
    amount = frappe.utils.flt(amount)
    if amount <= 0:
        frappe.throw(_("Amount must be greater than zero"))

    return {"valid": True}
```

### 6. Pagination

```python
@frappe.whitelist(methods=["GET"])
def paginated_list(doctype: str, page: int = 1, page_size: int = 20, filters: dict | None = None):
    """Get paginated results"""
    filters = filters or {}

    # Get total count
    total = frappe.db.count(doctype, filters=filters)

    # Get data (get_list enforces DocType permissions for the calling user)
    data = frappe.get_list(
        doctype,
        filters=filters,
        fields=["name"],
        start=(page - 1) * page_size,
        page_length=page_size,
        order_by="creation desc"
    )

    return {
        "data": data,
        "total": total,
        "page": page,
        "page_size": page_size,
        "total_pages": (total + page_size - 1) // page_size
    }
```

### 7. File Upload Handling

```python
@frappe.whitelist(methods=["POST"])
def upload_file():
    """Handle file upload"""
    from frappe.utils.file_manager import save_file

    if not frappe.request.

Related in Backend & APIs