Frappe_Claude_Skill_Package frappe-core-api
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/core/frappe-core-api" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-core-api && rm -rf "$T"
manifest:
skills/source/core/frappe-core-api/SKILL.mdsource content
Frappe API Patterns
Deterministic patterns for REST, RPC, and webhook integrations with Frappe.
Decision Tree
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
Authentication Methods
Token Auth (RECOMMENDED for integrations)
headers = { 'Authorization': 'token api_key:api_secret', 'Accept': 'application/json', 'Content-Type': 'application/json' }
Generate keys: User > Settings > API Access > Generate Keys. ALWAYS store API secret immediately — it is shown only once.
Basic Auth (alternative token format)
import base64 credentials = base64.b64encode(b'api_key:api_secret').decode() headers = {'Authorization': f'Basic {credentials}'}
OAuth 2.0 (third-party apps)
# Step 1: Authorization redirect GET /api/method/frappe.integrations.oauth2.authorize ?client_id={id}&response_type=code&scope=openid all &redirect_uri={uri}&state={random} # Step 2: Exchange code for token POST /api/method/frappe.integrations.oauth2.get_token grant_type=authorization_code&code={code} &redirect_uri={uri}&client_id={id} # Step 3: Use bearer token Authorization: Bearer {access_token} # Refresh token POST /api/method/frappe.integrations.oauth2.get_token grant_type=refresh_token&refresh_token={token}&client_id={id}
Session/Cookie Auth
session = requests.Session() session.post(url + '/api/method/login', json={'usr': 'email', 'pwd': 'pass'}) # Subsequent requests use session cookie automatically
Session cookies expire after ~3 days. NEVER use for long-running integrations.
REST API: Resource CRUD
Endpoints
| Operation | Method | v14 Endpoint | v15+ v2 Endpoint |
|---|---|---|---|
| List | GET | | |
| Create | POST | | |
| Read | GET | | |
| Update | PUT | | PATCH |
| Delete | DELETE | | DELETE |
| Copy | — | — | GET [v15+] |
| Doc Method | — | — | POST [v15+] |
ALWAYS include
Accept: application/json header — without it, Frappe MAY return HTML.
List Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
| JSON array | Fields to return | |
| JSON array | AND conditions | none |
| JSON array | OR conditions | none |
| string | Sort expression | |
| int | Pagination offset | |
| int | Page size | |
| int | Alias for limit_page_length [v15+] | — |
| bool | Show SQL in response | |
Filter Operators
filters = [["status", "=", "Open"]] filters = [["amount", ">", 1000]] filters = [["status", "in", ["Open", "Pending"]]] filters = [["date", "between", ["2024-01-01", "2024-12-31"]]] filters = [["reference", "is", "set"]] # NOT NULL filters = [["reference", "is", "not set"]] # IS NULL filters = [["name", "like", "%INV%"]] filters = [["status", "not in", ["Cancelled"]]]
Full operator list:
=, !=, >, <, >=, <=, like, not like, in, not in, is, between.
Pagination Pattern
import json, requests def get_all_records(doctype, headers, base_url, page_size=100): all_data, offset = [], 0 while True: params = { 'fields': json.dumps(["name", "modified"]), 'limit_start': offset, 'limit_page_length': page_size } resp = requests.get(f'{base_url}/api/resource/{doctype}', params=params, headers=headers) data = resp.json().get('data', []) if not data: break all_data.extend(data) if len(data) < page_size: break offset += page_size return all_data
Create with Child Table
requests.post(f'{base_url}/api/resource/Sales Order', json={ "customer": "CUST-001", "items": [ {"item_code": "ITEM-001", "qty": 5, "rate": 100}, {"item_code": "ITEM-002", "qty": 2, "rate": 250} ] }, headers=headers)
Update (Partial)
# Only specified fields are changed requests.put(f'{base_url}/api/resource/Customer/CUST-001', json={"customer_group": "Premium"}, headers=headers)
File Upload
requests.post(f'{base_url}/api/method/upload_file', files={'file': ('doc.pdf', open('doc.pdf', 'rb'), 'application/pdf')}, data={'doctype': 'Customer', 'docname': 'CUST-001', 'is_private': 1}, headers={'Authorization': 'token api_key:api_secret'}) # NOTE: Do NOT set Content-Type header — requests sets multipart boundary automatically
RPC API: Custom Methods
Server-Side Endpoint
@frappe.whitelist() def get_balance(customer): """GET /api/method/myapp.api.get_balance?customer=CUST-001""" return frappe.db.get_value("Customer", customer, "outstanding_amount") @frappe.whitelist(methods=["POST"]) def create_payment(customer, amount): """POST /api/method/myapp.api.create_payment""" if not frappe.has_permission("Payment Entry", "create"): frappe.throw(_("Not permitted"), frappe.PermissionError) pe = frappe.new_doc("Payment Entry") pe.party_type = "Customer" pe.party = customer pe.paid_amount = float(amount) pe.insert() return pe.name @frappe.whitelist(allow_guest=True) def public_status(): """No authentication required.""" return {"status": "ok"}
Decorator Options
| Option | Effect | Version |
|---|---|---|
| No authentication needed | All |
| Restrict HTTP methods | [v14+] |
| Skip XSS escaping on response | All |
Response Structure
// RPC success {"message": "return_value"} // REST success {"data": {...}} // Error {"exc_type": "ValidationError", "_server_messages": "[{\"message\": \"...\"}]"}
Client-Side Calls (JavaScript)
// RECOMMENDED: async/await with frappe.xcall const result = await frappe.xcall('myapp.api.get_balance', { customer: 'CUST-001' }); // Alternative: frappe.call with promise frappe.call({ method: 'myapp.api.get_balance', args: {customer: 'CUST-001'}, freeze: true, freeze_message: __('Loading...') }).then(r => console.log(r.message)); // Document method (frm.call) frm.call('get_linked_doc', {throw_if_missing: true}) .then(r => console.log(r.message));
Standard frappe.client Methods
| Method | Endpoint | Purpose |
|---|---|---|
| POST | Get single field value |
| POST | List with filters |
| POST | Get full document |
| POST | Create document |
| POST | Update document |
| POST | Delete document |
| POST | Submit document |
| POST | Cancel document |
| POST | Count documents |
Webhooks
Configure via Webhook DocType in the UI. Events:
| Event | Trigger |
|---|---|
| New document created |
| Every save |
| After submit (docstatus=1) |
| After cancel (docstatus=2) |
| Before delete |
| After amendment |
| On every change |
Security: ALWAYS set a Webhook Secret. Frappe adds
X-Frappe-Webhook-Signature header with base64-encoded HMAC-SHA256 of payload. Verify on receiving end.
Conditions: Use Jinja2 —
{{ doc.grand_total > 10000 }}.
See
references/webhooks-reference.md for complete handler examples.
HTTP Status Codes
| Code | Meaning | Common Cause |
|---|---|---|
| Success | — |
| Bad request | Validation error |
| Unauthorized | Missing or invalid auth |
| Forbidden | No permission for operation |
| Not found | Document does not exist |
| Expectation failed | Server exception (frappe.throw) |
| Rate limited | Too many requests |
| Server error | Unhandled exception |
Critical Rules
- ALWAYS include
header in API requestsAccept: application/json - ALWAYS add permission checks in
methods@frappe.whitelist() - ALWAYS validate and sanitize input in whitelisted methods
- ALWAYS use parameterized queries — NEVER string-interpolate SQL
- ALWAYS use
on externaltimeout=30
callsrequests - ALWAYS store credentials in
or env vars — NEVER hardcodefrappe.conf - ALWAYS verify webhook signatures with HMAC-SHA256
- ALWAYS paginate list responses — NEVER return unbounded result sets
- NEVER use
on state-changing endpointsallow_guest=True - NEVER log credentials or sensitive data
- NEVER use Administrator API keys for integrations — create dedicated API users
Anti-Patterns
| Do NOT | Do Instead |
|---|---|
| No permission check in whitelist | before action |
| Parameterized queries |
+ state change | Require authentication |
| Return all records without limit | Paginate with |
| Hardcode API credentials | |
| Synchronous heavy processing | for long tasks |
| No timeout on external calls | |
| Inconsistent response format | ALWAYS return |
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
(v1) | Yes | Yes | Yes |
(v2) | No | Yes | Yes |
| No | Yes | Yes |
| No | Yes | Yes |
alias parameter | No | Yes | Yes |
| PKCE for OAuth2 | Limited | Yes | Yes |
| Server Script rate limiting | No | Yes | Yes |
| Doc method via v2 URL | No | Yes | Yes |
Reference Files
| File | Contents |
|---|---|
| authentication-methods.md | Token, Session, OAuth2 with code examples |
| rest-api-reference.md | Complete REST CRUD with filters and pagination |
| rpc-api-reference.md | Whitelisted methods, frappe.call, frappe.xcall |
| webhooks-reference.md | Webhook config, security, handler examples |
| anti-patterns.md | Common mistakes with fixes |
| examples.md | Python/JS/cURL client implementations |
Related Skills
— Permission system for API endpointsfrappe-core-permissions
— Database queries behind API methodsfrappe-core-database
— Hook configuration for webhooksfrappe-syntax-hooks
— Controller methods called via APIfrappe-syntax-controllers
Verified against Frappe docs 2026-03-20 | Frappe v14/v15/v16