skills/source/core/frappe-core-api/SKILL.md
Use when building ERPNext/Frappe API integrations (v14/v15/v16) including REST API, RPC API, authentication, webhooks, and rate limiting. Covers external API calls, endpoint design, token/OAuth2/session authentication. Keywords: API integration, REST endpoint, webhook, token authentication,, how to connect, external API, send data to another system, API not working, 401 error. OAuth, frappe.call, external connection, rate limiting.
npx skillsauth add OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package frappe-core-apiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Deterministic patterns for REST, RPC, and webhook integrations with Frappe.
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
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.
import base64
credentials = base64.b64encode(b'api_key:api_secret').decode()
headers = {'Authorization': f'Basic {credentials}'}
# 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 = 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.
| Operation | Method | v14 Endpoint | v15+ v2 Endpoint |
|-----------|--------|--------------|------------------|
| 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} |
| Copy | — | — | GET /api/v2/document/{doctype}/{name}/copy [v15+] |
| Doc Method | — | — | POST /api/v2/document/{doctype}/{name}/method/{method} [v15+] |
ALWAYS include Accept: application/json header — without it, Frappe MAY return HTML.
| Parameter | Type | Description | Default |
|-----------|------|-------------|---------|
| fields | JSON array | Fields to return | ["name"] |
| filters | JSON array | AND conditions | none |
| or_filters | JSON array | OR conditions | none |
| order_by | string | Sort expression | modified desc |
| limit_start | int | Pagination offset | 0 |
| limit_page_length | int | Page size | 20 |
| limit | int | Alias for limit_page_length [v15+] | — |
| debug | bool | Show SQL in response | false |
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.
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
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)
# Only specified fields are changed
requests.put(f'{base_url}/api/resource/Customer/CUST-001',
json={"customer_group": "Premium"}, headers=headers)
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
@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"}
| Option | Effect | Version |
|--------|--------|---------|
| allow_guest=True | No authentication needed | All |
| methods=["POST"] | Restrict HTTP methods | [v14+] |
| xss_safe=True | Skip XSS escaping on response | All |
// RPC success
{"message": "return_value"}
// REST success
{"data": {...}}
// Error
{"exc_type": "ValidationError", "_server_messages": "[{\"message\": \"...\"}]"}
// 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));
| Method | Endpoint | Purpose |
|--------|----------|---------|
| frappe.client.get_value | POST | Get single field value |
| frappe.client.get_list | POST | List with filters |
| frappe.client.get | POST | Get full document |
| frappe.client.insert | POST | Create document |
| frappe.client.save | POST | Update document |
| frappe.client.delete | POST | Delete document |
| frappe.client.submit | POST | Submit document |
| frappe.client.cancel | POST | Cancel document |
| frappe.client.get_count | POST | Count documents |
Configure via Webhook DocType in the UI. Events:
| Event | Trigger |
|-------|---------|
| after_insert | New document created |
| on_update | Every save |
| on_submit | After submit (docstatus=1) |
| on_cancel | After cancel (docstatus=2) |
| on_trash | Before delete |
| on_update_after_submit | After amendment |
| on_change | 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.
| Code | Meaning | Common Cause |
|------|---------|--------------|
| 200 | Success | — |
| 400 | Bad request | Validation error |
| 401 | Unauthorized | Missing or invalid auth |
| 403 | Forbidden | No permission for operation |
| 404 | Not found | Document does not exist |
| 417 | Expectation failed | Server exception (frappe.throw) |
| 429 | Rate limited | Too many requests |
| 500 | Server error | Unhandled exception |
Accept: application/json header in API requests@frappe.whitelist() methodstimeout=30 on external requests callsfrappe.conf or env vars — NEVER hardcodeallow_guest=True on state-changing endpoints| Do NOT | Do Instead |
|--------|------------|
| No permission check in whitelist | frappe.has_permission() before action |
| frappe.db.sql(f"...{user_input}") | Parameterized %s queries |
| allow_guest=True + state change | Require authentication |
| Return all records without limit | Paginate with limit_page_length |
| Hardcode API credentials | frappe.conf.get("api_key") |
| Synchronous heavy processing | frappe.enqueue() for long tasks |
| No timeout on external calls | requests.get(url, timeout=30) |
| Inconsistent response format | ALWAYS return {"status": "...", "data": ...} |
| Feature | v14 | v15 | v16 |
|---------|-----|-----|-----|
| /api/resource/ (v1) | Yes | Yes | Yes |
| /api/v2/document/ (v2) | No | Yes | Yes |
| /api/v2/doctype/{dt}/meta | No | Yes | Yes |
| /api/v2/doctype/{dt}/count | No | Yes | Yes |
| limit 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 |
| 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 |
frappe-core-permissions — Permission system for API endpointsfrappe-core-database — Database queries behind API methodsfrappe-syntax-hooks — Hook configuration for webhooksfrappe-syntax-controllers — Controller methods called via APIVerified against Frappe docs 2026-03-20 | Frappe v14/v15/v16
tools
Use when implementing OAuth providers, Connected Apps, Webhooks, Payment Gateways, or Data Import/Export in Frappe. Prevents authentication failures from wrong OAuth flow, missed webhook deliveries, and data corruption during bulk imports. Covers OAuth2 provider/client, Connected App DocType, Webhook DocType, Payment Gateway integration, Data Import, Data Export, frappe.integrations module. Keywords: OAuth, Connected App, Webhook, Payment Gateway, Data Import, Data Export, integration, API key, OAuth2, webhook trigger, connect to external service, OAuth setup, webhook configuration, import data, export data..
development
Use when implementing hooks.py configurations in a Frappe custom app. Covers step-by-step workflows for doc_events, scheduler_events, override/extend_doctype_class, permission hooks, extend_bootinfo, fixtures, asset injection, website hooks, and doctype_js. Prevents broken transactions, missed migrations, and multi-app conflicts. Keywords: hooks.py, doc_events, scheduler_events, override doctype,, how to add hook, when to use doc_events, scheduler setup, override existing behavior. extend doctype class, permission hook, scheduler job, fixtures, doctype_js, extend_bootinfo, website hooks.
development
Use when building a custom Frappe app from scratch. Covers bench new-app walkthrough, app structure decisions, adding DocTypes, hooks, patches, fixtures management, development workflow (bench migrate, build, clear-cache), testing, packaging, installing on another site, version management, and app dependencies for v14/v15/v16. Keywords: create custom app, new frappe app, bench new-app, app structure, module creation, doctype creation, fixtures, patches, deployment, packaging, data migration, patch file, patches.txt, migrate data between DocTypes, create new app from scratch.
development
Use when building Document Controllers in a custom Frappe app: file creation, lifecycle hooks, validation, autoname, submittable workflows, controller override, child table controllers, flags system, migration from hooks.py and Server Scripts. Keywords: how to implement controller, which hook to use, validate vs on_update, override controller, submittable document, autoname, flags, extend_doctype_class, controller testing, child table controller, which hook to use, when does validate run, how to override save, document lifecycle.