Loading...
Loading...
Build REST and RPC APIs in Frappe including whitelisted methods, authentication, and permission handling. Use when creating custom endpoints, integrating with external systems, or exposing business logic via API.
npx skill4agent add lubusin/agent-skills frappe-api-development@frappe.whitelist| Need | Pattern |
|---|---|
| DocType CRUD | Use built-in REST API |
| Custom action | RPC with |
| External callback | Webhook DocType |
| Batch operations | Background job + status endpoint |
# Create
POST /api/resource/Customer
{"customer_name": "Acme Corp"}
# Read
GET /api/resource/Customer/CUST-001
# Update
PUT /api/resource/Customer/CUST-001
{"customer_name": "Acme Corporation"}
# Delete
DELETE /api/resource/Customer/CUST-001
# List with filters
GET /api/resource/Customer?filters=[["status","=","Active"]]# my_app/api.py
import frappe
@frappe.whitelist()
def process_order(order_id, action):
"""Process an order with the given action."""
# Always verify permissions
doc = frappe.get_doc("Sales Order", order_id)
if not frappe.has_permission("Sales Order", "write", doc):
frappe.throw("Not permitted", frappe.PermissionError)
# Business logic
if action == "approve":
doc.status = "Approved"
doc.save()
return {"status": "success", "order": doc.name}
@frappe.whitelist(allow_guest=True)
def public_endpoint():
"""Public endpoint - no auth required."""
return {"message": "Hello, World!"}POST /api/method/my_app.api.process_order
{"order_id": "SO-001", "action": "approve"}# Header format
Authorization: token api_key:api_secretAuthorization: Bearer <token>@frappe.whitelist()
def sensitive_action(docname):
doc = frappe.get_doc("My DocType", docname)
# Check document-level permission
if not frappe.has_permission("My DocType", "write", doc):
frappe.throw("Not permitted", frappe.PermissionError)
# Check role-based permission
if "Manager" not in frappe.get_roles():
frappe.throw("Manager role required")
# Proceed with action
...@frappe.whitelist()
def create_item(name, qty, price):
# Validate required fields
if not name:
frappe.throw("Name is required")
# Validate types
qty = frappe.utils.cint(qty)
price = frappe.utils.flt(price)
# Validate ranges
if qty <= 0:
frappe.throw("Quantity must be positive")
# Proceed
...return {
"status": "success",
"data": {...}
}# User-facing error
frappe.throw("Validation failed", title="Error")
# Permission error
frappe.throw("Not allowed", frappe.PermissionError)
# Standard exceptions become {"exc_type": "...", "exc": "..."}@frappe.whitelist()
def start_export(filters):
job = frappe.enqueue(
"my_app.jobs.run_export",
filters=filters,
queue="long",
timeout=600
)
return {"job_id": job.id}
@frappe.whitelist()
def check_job_status(job_id):
from frappe.utils.background_jobs import get_job
job = get_job(job_id)
return {"status": job.get_status()}bench --site <site> console@frappe.whitelist()bench --site <site> show-logfrappe.has_permission()frappe.db.escape()eval()frappe.throw()| Mistake | Why It Fails | Fix |
|---|---|---|
Missing | Method returns "Method not found" error | Add decorator to expose method via API |
| Using GET for mutations | Violates REST conventions, CSRF issues | Use POST/PUT/DELETE for data changes |
| Not handling errors | 500 errors expose stack traces | Wrap in try/except, use |
| Exposing sensitive data | Security breach | Filter response fields, check permissions |
Missing | Public endpoints return 403 | Add |
| SQL injection in queries | Database compromise | Use Query Builder or |