Create secure REST API endpoints for Frappe Framework v15 with proper authentication, permissions, and validation. Triggers: "create api", "new endpoint", "frappe api", "rest api", "whitelist method", "/frappe-api". Generates v2 API compatible endpoints with type validation and security best practices.
/plugin marketplace add sergio-bershadsky/ai/plugin install frappe-dev@bershadsky-claude-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Create secure, well-documented REST API endpoints for Frappe Framework v15 following best practices for authentication, permission checking, and input validation.
/frappe-api <endpoint_name> [--doctype <doctype>] [--public]
Examples:
/frappe-api get_dashboard_stats
/frappe-api create_order --doctype "Sales Order"
/frappe-api webhook_handler --public
Ask the user for:
get_dashboard_stats)Create the API specification:
Endpoint: /api/method/<app>.<module>.api.<endpoint_name>
Methods: GET, POST
Auth: Token | Session
Rate Limit: 100 req/min (if applicable)
Parameters:
- name: param1
type: string
required: true
description: Description of param1
- name: param2
type: integer
required: false
default: 10
Response:
200:
description: Success
schema:
message: object
400:
description: Validation Error
403:
description: Permission Denied
Create <app>/<module>/api/<endpoint_name>.py:
"""
<Endpoint Name> API
<Brief description of what this API does>
Endpoints:
GET/POST /api/method/<app>.<module>.api.<endpoint_name>.<method_name>
Authentication:
Token: Authorization: token api_key:api_secret
Session: Cookie-based after login
Example:
curl -X POST "https://site.com/api/method/<app>.<module>.api.<endpoint_name>.create" \
-H "Authorization: token api_key:api_secret" \
-H "Content-Type: application/json" \
-d '{"title": "Test"}'
"""
import frappe
from frappe import _
from frappe.utils import cint, cstr, flt
from typing import Optional, Any
from <app>.<module>.services.<service>_service import <Service>Service
# ──────────────────────────────────────────────────────────────────────────────
# API Endpoints
#
# v15 TYPE ANNOTATION VALIDATION:
# Frappe v15 automatically validates function parameter types based on
# Python type hints. For example, if you declare `limit: int`, passing
# a non-integer will raise a validation error automatically.
#
# TRANSACTION HANDLING:
# Frappe automatically commits on successful POST/PUT requests and
# rolls back on exceptions. Manual frappe.db.commit() is rarely needed.
# ──────────────────────────────────────────────────────────────────────────────
@frappe.whitelist()
def get(name: str) -> dict:
"""
Get single document by name.
Args:
name: Document name/ID
Returns:
Document data
Raises:
frappe.DoesNotExistError: Document not found
frappe.PermissionError: No read permission
Example:
GET /api/method/<app>.<module>.api.<endpoint>.get?name=DOC-00001
"""
_check_permission("<DocType>", "read")
service = <Service>Service()
return {
"success": True,
"data": service.get(name)
}
@frappe.whitelist()
def get_list(
status: Optional[str] = None,
limit: int = 20,
offset: int = 0
) -> dict:
"""
Get list of documents with optional filtering.
Args:
status: Filter by status
limit: Maximum records to return (default: 20, max: 100)
offset: Skip N records for pagination
Returns:
List of documents with pagination info
Example:
GET /api/method/<app>.<module>.api.<endpoint>.get_list?status=Draft&limit=10
"""
_check_permission("<DocType>", "read")
# Validate and sanitize inputs
limit = min(cint(limit) or 20, 100) # Cap at 100
offset = max(cint(offset), 0)
service = <Service>Service()
filters = {}
if status:
filters["status"] = status
data = service.repo.get_list(
filters=filters,
fields=["name", "title", "status", "date", "modified"],
limit=limit,
offset=offset
)
total = service.repo.get_count(filters)
return {
"success": True,
"data": data,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total
}
}
@frappe.whitelist(methods=["POST"])
def create(
title: str,
date: Optional[str] = None,
description: Optional[str] = None
) -> dict:
"""
Create new document.
Args:
title: Document title (required)
date: Date in YYYY-MM-DD format
description: Optional description
Returns:
Created document data
Raises:
frappe.ValidationError: Invalid input data
frappe.PermissionError: No create permission
Example:
POST /api/method/<app>.<module>.api.<endpoint>.create
Body: {"title": "New Document", "date": "2024-01-15"}
"""
_check_permission("<DocType>", "create")
# Validate required fields
if not title or not cstr(title).strip():
frappe.throw(_("Title is required"), frappe.ValidationError)
service = <Service>Service()
result = service.create({
"title": cstr(title).strip(),
"date": date or frappe.utils.today(),
"description": description
})
frappe.db.commit()
return {
"success": True,
"message": _("Document created successfully"),
"data": result
}
@frappe.whitelist(methods=["PUT", "POST"])
def update(
name: str,
title: Optional[str] = None,
status: Optional[str] = None,
description: Optional[str] = None
) -> dict:
"""
Update existing document.
Args:
name: Document name (required)
title: New title
status: New status
description: New description
Returns:
Updated document data
Example:
PUT /api/method/<app>.<module>.api.<endpoint>.update
Body: {"name": "DOC-00001", "title": "Updated Title"}
"""
_check_permission("<DocType>", "write")
if not name:
frappe.throw(_("Document name is required"), frappe.ValidationError)
# Build update data from provided fields
update_data = {}
if title is not None:
update_data["title"] = cstr(title).strip()
if status is not None:
update_data["status"] = status
if description is not None:
update_data["description"] = description
if not update_data:
frappe.throw(_("No fields to update"), frappe.ValidationError)
service = <Service>Service()
result = service.update(name, update_data)
frappe.db.commit()
return {
"success": True,
"message": _("Document updated successfully"),
"data": result
}
@frappe.whitelist(methods=["DELETE", "POST"])
def delete(name: str) -> dict:
"""
Delete document.
Args:
name: Document name to delete
Returns:
Success confirmation
Example:
DELETE /api/method/<app>.<module>.api.<endpoint>.delete?name=DOC-00001
"""
_check_permission("<DocType>", "delete")
if not name:
frappe.throw(_("Document name is required"), frappe.ValidationError)
service = <Service>Service()
service.repo.delete(name)
frappe.db.commit()
return {
"success": True,
"message": _("Document deleted successfully")
}
@frappe.whitelist(methods=["POST"])
def submit(name: str) -> dict:
"""
Submit document for processing.
Args:
name: Document name to submit
Returns:
Submitted document data
Example:
POST /api/method/<app>.<module>.api.<endpoint>.submit
Body: {"name": "DOC-00001"}
"""
_check_permission("<DocType>", "submit")
service = <Service>Service()
result = service.submit(name)
frappe.db.commit()
return {
"success": True,
"message": _("Document submitted successfully"),
"data": result
}
@frappe.whitelist(methods=["POST"])
def cancel(name: str, reason: Optional[str] = None) -> dict:
"""
Cancel submitted document.
Args:
name: Document name to cancel
reason: Cancellation reason
Returns:
Cancelled document data
Example:
POST /api/method/<app>.<module>.api.<endpoint>.cancel
Body: {"name": "DOC-00001", "reason": "Customer request"}
"""
_check_permission("<DocType>", "cancel")
service = <Service>Service()
result = service.cancel(name, reason)
frappe.db.commit()
return {
"success": True,
"message": _("Document cancelled successfully"),
"data": result
}
# ──────────────────────────────────────────────────────────────────────────────
# Bulk Operations
# ──────────────────────────────────────────────────────────────────────────────
@frappe.whitelist(methods=["POST"])
def bulk_update_status(names: list[str], status: str) -> dict:
"""
Bulk update status for multiple documents.
Args:
names: List of document names
status: New status to set
Returns:
Number of documents updated
Example:
POST /api/method/<app>.<module>.api.<endpoint>.bulk_update_status
Body: {"names": ["DOC-001", "DOC-002"], "status": "Completed"}
"""
_check_permission("<DocType>", "write")
if not names or not isinstance(names, list):
frappe.throw(_("Names must be a non-empty list"), frappe.ValidationError)
valid_statuses = ["Draft", "Pending", "Completed", "Cancelled"]
if status not in valid_statuses:
frappe.throw(
_("Invalid status. Must be one of: {0}").format(", ".join(valid_statuses)),
frappe.ValidationError
)
service = <Service>Service()
count = service.repo.bulk_update_status(names, status)
frappe.db.commit()
return {
"success": True,
"message": _("{0} documents updated").format(count),
"data": {"updated_count": count}
}
# ──────────────────────────────────────────────────────────────────────────────
# Helper Functions
# ──────────────────────────────────────────────────────────────────────────────
def _check_permission(doctype: str, ptype: str, doc: Any = None) -> None:
"""
Check if current user has permission.
Args:
doctype: DocType to check
ptype: Permission type (read, write, create, delete, submit, cancel)
doc: Optional specific document
Raises:
frappe.PermissionError: If permission denied
"""
if not frappe.has_permission(doctype, ptype, doc=doc):
frappe.throw(
_("You don't have permission to {0} {1}").format(ptype, doctype),
frappe.PermissionError
)
def _validate_request_data(data: dict, required: list[str]) -> None:
"""
Validate request data has required fields.
Args:
data: Request data dict
required: List of required field names
Raises:
frappe.ValidationError: If required fields missing
"""
missing = [f for f in required if not data.get(f)]
if missing:
frappe.throw(
_("Missing required fields: {0}").format(", ".join(missing)),
frappe.ValidationError
)
# ──────────────────────────────────────────────────────────────────────────────
# Public Endpoints (No Auth Required - Use with Caution!)
# ──────────────────────────────────────────────────────────────────────────────
@frappe.whitelist(allow_guest=True)
def ping() -> dict:
"""
Health check endpoint (public).
Returns:
Server status
Example:
GET /api/method/<app>.<module>.api.<endpoint>.ping
"""
return {
"success": True,
"message": "pong",
"timestamp": frappe.utils.now()
}
For v2 REST API pattern, create custom routes in hooks.py:
# hooks.py
# Override standard DocType REST endpoints
override_doctype_dashboards = {
"<DocType>": "<app>.<module>.api.<endpoint>.get_doctype_dashboard"
}
# Custom website routes for cleaner URLs
website_route_rules = [
{"from_route": "/api/v2/<app>/<endpoint>", "to_route": "<app>.<module>.api.<endpoint>.handle_v2"},
]
Create <app>/<module>/api/test_<endpoint_name>.py:
"""
Tests for <Endpoint Name> API
"""
import frappe
from frappe.tests import IntegrationTestCase
class TestAPI<EndpointName>(IntegrationTestCase):
"""API integration tests."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_user = cls._create_test_user()
cls.test_doc = cls._create_test_document()
@classmethod
def _create_test_user(cls):
"""Create test user with API access."""
if frappe.db.exists("User", "test_api@example.com"):
return frappe.get_doc("User", "test_api@example.com")
user = frappe.get_doc({
"doctype": "User",
"email": "test_api@example.com",
"first_name": "Test",
"last_name": "API User",
"send_welcome_email": 0
}).insert(ignore_permissions=True)
user.add_roles("System Manager")
return user
@classmethod
def _create_test_document(cls):
"""Create test document."""
return frappe.get_doc({
"doctype": "<DocType>",
"title": "API Test Document",
"date": frappe.utils.today()
}).insert()
def test_get_returns_document(self):
"""Test GET endpoint returns document."""
from <app>.<module>.api.<endpoint_name> import get
frappe.set_user(self.test_user.name)
result = get(self.test_doc.name)
self.assertTrue(result.get("success"))
self.assertIsNotNone(result.get("data"))
def test_get_list_with_pagination(self):
"""Test GET list with pagination."""
from <app>.<module>.api.<endpoint_name> import get_list
frappe.set_user(self.test_user.name)
result = get_list(limit=5, offset=0)
self.assertTrue(result.get("success"))
self.assertIn("pagination", result)
self.assertLessEqual(len(result["data"]), 5)
def test_create_validates_input(self):
"""Test CREATE validates required fields."""
from <app>.<module>.api.<endpoint_name> import create
frappe.set_user(self.test_user.name)
with self.assertRaises(frappe.ValidationError):
create(title="") # Empty title should fail
def test_create_returns_document(self):
"""Test CREATE returns new document."""
from <app>.<module>.api.<endpoint_name> import create
frappe.set_user(self.test_user.name)
result = create(title="New Test Doc", date=frappe.utils.today())
self.assertTrue(result.get("success"))
self.assertIsNotNone(result["data"].get("name"))
def test_unauthorized_access_denied(self):
"""Test unauthenticated access is denied."""
from <app>.<module>.api.<endpoint_name> import get
frappe.set_user("Guest")
with self.assertRaises(frappe.PermissionError):
get(self.test_doc.name)
def test_ping_public_access(self):
"""Test ping endpoint is publicly accessible."""
from <app>.<module>.api.<endpoint_name> import ping
frappe.set_user("Guest")
result = ping()
self.assertTrue(result.get("success"))
self.assertEqual(result.get("message"), "pong")
## API Endpoint Preview
**Module:** <app>.<module>.api.<endpoint_name>
**Base URL:** /api/method/<app>.<module>.api.<endpoint_name>
### Endpoints:
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | .get | Token/Session | Get single document |
| GET | .get_list | Token/Session | List with pagination |
| POST | .create | Token/Session | Create document |
| PUT | .update | Token/Session | Update document |
| DELETE | .delete | Token/Session | Delete document |
| POST | .submit | Token/Session | Submit for processing |
| POST | .cancel | Token/Session | Cancel document |
| POST | .bulk_update_status | Token/Session | Bulk status update |
| GET | .ping | Public | Health check |
### Authentication:
```bash
# Token auth (recommended for integrations)
curl -H "Authorization: token api_key:api_secret" \
https://site.com/api/method/<endpoint>
# Session auth (for browser clients)
# First login, then use session cookie
📁 <module>/api/ ├── 📄 init.py ├── 📄 <endpoint_name>.py └── 📄 test_<endpoint_name>.py
Create this API module?
### Step 7: Execute and Verify
After approval, create files and run tests:
```bash
bench --site <site> run-tests --module "<app>.<module>.api.test_<endpoint_name>"
## API Created
**Module:** <app>.<module>.api.<endpoint_name>
**Endpoints:** 9
### Files Created:
- ✅ <endpoint_name>.py (API endpoints)
- ✅ test_<endpoint_name>.py (API tests)
- ✅ Updated __init__.py
### cURL Examples:
```bash
# Get document
curl -X GET "https://site.com/api/method/<app>.<module>.api.<endpoint>.get?name=DOC-001" \
-H "Authorization: token api_key:api_secret"
# Create document
curl -X POST "https://site.com/api/method/<app>.<module>.api.<endpoint>.create" \
-H "Authorization: token api_key:api_secret" \
-H "Content-Type: application/json" \
-d '{"title": "New Document"}'
bench --site <site> run-tests --module <test_module>
## Rules
1. **Always Check Permissions** — Every endpoint must call `_check_permission()` first
2. **Validate All Input** — Never trust user input, validate and sanitize everything
3. **Type Annotations** — Use Python type hints for v15 auto-validation
4. **Transaction Handling** — Frappe auto-commits on successful requests; manual `frappe.db.commit()` rarely needed except in background jobs
5. **Public Endpoints** — Use `allow_guest=True` sparingly, only for truly public data
6. **Error Handling** — Use `frappe.throw()` with appropriate exception types
7. **Documentation** — Every endpoint must have docstring with Args/Returns/Example
8. **ALWAYS Confirm** — Never create files without explicit user approval
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.