Generate Frappe API Handler
frappe-api-handler
Generate whitelisted API methods and REST endpoints for standard Frappe and microservices.
SKILL.md
Full skill instructions
Generate Frappe API Handler
Create secure API endpoints for Frappe applications. Supports both standard Frappe and microservices.
When to Use
- Creating custom API endpoints
- Building REST APIs
- Creating whitelisted methods
- Building microservice endpoints
Core Patterns
1. Standard Frappe API
@frappe.whitelist()
def get_customer_details(customer_name):
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
}
@frappe.whitelist(allow_guest=True)
def public_api():
return {"message": "Public data"}
2. REST Patterns
GET with pagination:
@frappe.whitelist()
def get_items(filters=None, limit=20, page=1):
filters = frappe.parse_json(filters) if isinstance(filters, str) else filters or {}
if not frappe.has_permission("Item", "read"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
items = frappe.get_all("Item", filters=filters, limit=limit, limit_start=(page-1)*limit)
return {"items": items, "total": frappe.db.count("Item", filters=filters)}
POST:
@frappe.whitelist()
def create_order(order_data):
data = frappe.parse_json(order_data) if isinstance(order_data, str) else order_data
if not data.get("customer"):
frappe.throw(_("Customer is required"))
so = frappe.get_doc({"doctype": "Sales Order", **data})
so.insert()
return {"success": True, "name": so.name}
PUT/DELETE: Similar pattern - get doc, update/delete, return result
3. Microservice API
@app.secure_route('/api/customers', methods=['GET'])
def list_customers(user):
tenant_id = get_current_tenant_id()
app.set_tenant_id(tenant_id)
customers = app.tenant_db.get_all('Customer', filters=request.args.get('filters', {}))
return {"data": customers}
@app.secure_route('/api/customers', methods=['POST'])
def create_customer(user):
tenant_id = get_current_tenant_id()
app.set_tenant_id(tenant_id)
customer = app.tenant_db.insert_doc('Customer', request.json)
return {"success": True, "data": customer.as_dict()}, 201
4. Error Handling
@frappe.whitelist()
def api_with_errors(param):
try:
if not param:
frappe.throw(_("Required"), frappe.ValidationError)
return {"success": True, "data": process(param)}
except frappe.ValidationError as e:
return {"success": False, "error": str(e), "code": "VALIDATION_ERROR"}, 400
except frappe.PermissionError as e:
return {"success": False, "error": str(e), "code": "PERMISSION_ERROR"}, 403
except frappe.DoesNotExistError as e:
return {"success": False, "error": str(e), "code": "NOT_FOUND"}, 404
except Exception as e:
frappe.log_error(f"API error: {e}")
return {"success": False, "error": "Internal error", "code": "INTERNAL_ERROR"}, 500
5. Authentication
Session-based:
@frappe.whitelist()
def authenticated_api():
return {"user": frappe.session.user}
API Key:
@frappe.whitelist()
def api_key_auth():
api_key = frappe.get_request_header("X-API-Key")
if not api_key:
frappe.throw(_("API Key required"), frappe.AuthenticationError)
# Validate key
return {"authenticated": True}
Key Patterns
- Always check permissions with
frappe.has_permission() - Parse JSON strings with
frappe.parse_json() - Return proper HTTP status codes
- Validate input before processing
- Use
app.tenant_dbin microservices - Log errors with
frappe.log_error()
Best Practices
- Use
@frappe.whitelist()decorator - Check permissions before operations
- Validate all input
- Use
frappe._()for translatable messages - Consistent response format
- Document with docstrings
Remember: This skill is model-invoked. Claude will use it autonomously when detecting API development tasks.
Decision Tree & Reference
Cross-package reference from frappe-core-api and frappe-syntax-whitelisted: where to expose logic, how clients call Frappe, and rules that extend the patterns above.
Decision tree: API surfaces (integrations & clients)
What do you need?
├── CRUD on documents (external client)
│ ├── v14: REST /api/resource/{doctype}
│ └── v15+: REST /api/v2/document/{doctype} (new) or /api/resource/ (still works)
│
├── Call custom server logic (external client)
│ └── RPC: POST /api/method/{dotted.path.to.function}
│
├── Notify external systems on document events
│ └── Webhooks (configured in UI or via DocType)
│
├── Client-side calls (JavaScript in Frappe desk)
│ ├── frappe.xcall() — async/await (RECOMMENDED)
│ └── frappe.call() — callback/promise pattern
│
└── Authentication method?
├── Server-to-server integration → Token Auth (RECOMMENDED)
├── Third-party app / mobile → OAuth 2.0
├── Browser session (short-lived) → Session/Cookie Auth
└── Quick scripting / testing → Token Auth
Decision tree: @frappe.whitelist() design
What kind of endpoint?
|
+-- Standalone API (utility, integration, dashboard)?
| --> @frappe.whitelist() on a module-level function
| --> Call via: frappe.call('myapp.api.function')
| --> URL: /api/method/myapp.api.function
|
+-- Document-specific action?
| --> @frappe.whitelist() on a Document class method
| --> Call via: frm.call('method_name')
| --> URL: /api/method/run_doc_method (internal)
|
+-- Server Script (no-code)?
--> Use Server Script DocType instead (no decorator needed)
Who may call the API?
|
+-- Anyone (including guests)?
| --> allow_guest=True + thorough input validation + rate limiting
|
+-- Logged-in users only?
+-- Specific role? --> frappe.only_for("RoleName")
+-- DocType-level? --> frappe.has_permission(doctype, ptype, throw=True)
+-- Document-level? --> frappe.has_permission(doctype, ptype, doc, throw=True)
Which HTTP methods?
|
+-- Read only? --> methods=["GET"]
+-- Write only? --> methods=["POST"]
+-- Both? --> methods=["GET","POST"] or default
Quick reference tables
Module-level RPC / Desk entrypoint: /api/method/{dotted.module.path.function_name} (returns JSON with message carrying the Python return value on success.)
| RPC decorator option | Effect | Notes |
|---|---|---|
allow_guest=True | No login required | ALWAYS tighten validation + consider @rate_limit |
xss_safe=True | Skips XSS escaping | NEVER without fully trusted/sanitized output |
methods=[...] | Limits HTTP verbs | e.g. ["POST"] for mutations |
force_types=True | [v15+] Require param annotations | Missing annotations → FrappeTypeError |
| REST operation | Method | v14 | v15+ (v2) |
|---|---|---|---|
| List | GET | /api/resource/{doctype} | /api/v2/document/{doctype} |
| Create | POST | /api/resource/{doctype} | /api/v2/document/{doctype} |
| Read | GET | /api/resource/{doctype}/{name} | /api/v2/document/{doctype}/{name} |
| Update | PUT | /api/resource/{doctype}/{name} | PATCH /api/v2/document/{doctype}/{name} |
| Delete | DELETE | /api/resource/{doctype}/{name} | DELETE /api/v2/document/{doctype}/{name} |
| Code | Typical meaning |
|---|---|
| 200 | Success |
| 400 | Bad request / validation |
| 401 | Not authenticated |
| 403 | Authenticated but not permitted |
| 404 | Missing doc/resource |
| 417 | frappe.throw / expectation failed |
| 429 | Rate limited |
| 500 | Unhandled server error |
frappe.client RPC (Desk / token) | Purpose |
|---|---|
get_value, get_list, get | Read |
insert, save, submit, cancel, delete | Writes / workflow |
get_count | Count with filters |
List query params clients often send: fields, filters, or_filters, order_by, limit_start, limit_page_length (limit alias on v15+), optional debug.
ALWAYS / NEVER (additive — extends Key Patterns above)
- ALWAYS send
Accept: application/jsonwhen calling Frappe REST from scripts or services (otherwise responses may be HTML). - ALWAYS use parameterized DB access — NEVER build SQL by interpolating user input.
- ALWAYS set a timeout (e.g.
timeout=30) on outboundrequeststo third parties. - ALWAYS keep secrets in
frappe.confor environment variables — NEVER hardcode keys in repo. - ALWAYS paginate list-style API responses intended for arbitrary-sized data.
- ALWAYS verify webhook HMAC (
X-Frappe-Webhook-Signature) when exposing inbound webhook receivers. - NEVER use
allow_guest=Trueon endpoints that change data or privileged state without extra gates (validation + rate limit + minimal surface). - NEVER log credentials or PCI/secret-bearing payloads.
- NEVER use Administrator API keys for integrations — dedicate least-privilege API users.
- NEVER rely on
@frappe.whitelist()alone for authorization — treat login as authentication only; still enforcefrappe.has_permission/frappe.only_foras needed (see Key Patterns above). - NEVER return raw stack traces to API clients — use
frappe.log_errorand a safe user-facing message. - NEVER use
ignore_permissions=Truewithout an explicit preceding role/security guard. - ALWAYS apply
@rate_limiton guest-visible endpoints to reduce abuse.
Critical Rules (sources — condensed)
From frappe-core-api: prefer token auth for integrations; store API secrets immediately when generated (shown once); session cookies expire (~3 days) — not for long-lived integrations; set Webhook secrets; use Jinja2 conditions on webhook conditions; outbound integration calls need timeouts and safe credential storage.
From frappe-syntax-whitelisted: HTTP parameters arrive as strings — coerce types and frappe.parse_json for JSON blobs; Desk callers should JSON.stringify complex frappe.call args; frappe.form_dict for dynamic param maps; [v15+] optional force_types and site hook require_type_annotated_api_methods enforce annotations; frappe.local.response["http_status_code"] for non-default RPC HTTP status where applicable.
