This handles both REST and RPC patterns in Frappe, covering the built-in DocType CRUD endpoints and custom whitelisted methods. You'll reach for it when building external integrations, creating webhook receivers, or exposing business logic to frontends. The procedure walks through authentication options (API keys, bearer tokens), permission checking with frappe.has_permission(), and input validation patterns. Notable inclusion: guidance on when to use background jobs for long operations and return status endpoints instead of blocking. The guardrails section is solid on SQL injection and permission bypasses, which are real risks if you're not careful with frappe.whitelist decorators.
npx -y skills add lubusin/agent-skills --skill frappe-api-development --agent claude-codeInstalls into .claude/skills of the current project.
Build secure, well-designed APIs using Frappe's REST and RPC patterns.
@frappe.whitelist)| Need | Pattern |
|---|---|
| DocType CRUD | Use built-in REST API |
| Custom action | RPC with @frappe.whitelist |
| External callback | Webhook DocType |
| Batch operations | Background job + status endpoint |
Frappe provides automatic REST endpoints for all DocTypes:
# 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"]]
Create whitelisted methods in your app:
# 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!"}
Call via:
POST /api/method/my_app.api.process_order
{"order_id": "SO-001", "action": "approve"}
API Key + Secret (recommended for integrations):
# Header format
Authorization: token api_key:api_secret
Bearer Token:
Authorization: Bearer <token>
Session (for logged-in users): Automatic via cookies.
ALWAYS check permissions in RPC methods:
@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
...
Success response:
return {
"status": "success",
"data": {...}
}
Error handling:
# 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 → test endpoint manually@frappe.whitelist() decorator and user permissionsbench --site <site> show-logfrappe.has_permission() explicitly in whitelisted methodsfrappe.db.escape() for SQL, avoid eval() and dynamic code executionfrappe.throw() with proper HTTP status codes| Mistake | Why It Fails | Fix |
|---|---|---|
Missing @frappe.whitelist() | 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 frappe.throw() |
| Exposing sensitive data | Security breach | Filter response fields, check permissions |
Missing allow_guest=True | Public endpoints return 403 | Add @frappe.whitelist(allow_guest=True) for unauthenticated access |
| SQL injection in queries | Database compromise | Use Query Builder or frappe.db.escape() |
prisma/skills
firebase/agent-skills
Dexploarer/hyper-forge
itsmostafa/aws-agent-skills
prisma/skills