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.md
source 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

OperationMethodv14 Endpointv15+ v2 Endpoint
ListGET
/api/resource/{doctype}
/api/v2/document/{doctype}
CreatePOST
/api/resource/{doctype}
/api/v2/document/{doctype}
ReadGET
/api/resource/{doctype}/{name}
/api/v2/document/{doctype}/{name}
UpdatePUT
/api/resource/{doctype}/{name}
PATCH
/api/v2/document/{doctype}/{name}
DeleteDELETE
/api/resource/{doctype}/{name}
DELETE
/api/v2/document/{doctype}/{name}
CopyGET
/api/v2/document/{doctype}/{name}/copy
[v15+]
Doc MethodPOST
/api/v2/document/{doctype}/{name}/method/{method}
[v15+]

ALWAYS include

Accept: application/json
header — without it, Frappe MAY return HTML.

List Parameters

ParameterTypeDescriptionDefault
fields
JSON arrayFields to return
["name"]
filters
JSON arrayAND conditionsnone
or_filters
JSON arrayOR conditionsnone
order_by
stringSort expression
modified desc
limit_start
intPagination offset
0
limit_page_length
intPage size
20
limit
intAlias for limit_page_length [v15+]
debug
boolShow SQL in response
false

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

OptionEffectVersion
allow_guest=True
No authentication neededAll
methods=["POST"]
Restrict HTTP methods[v14+]
xss_safe=True
Skip XSS escaping on responseAll

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

MethodEndpointPurpose
frappe.client.get_value
POSTGet single field value
frappe.client.get_list
POSTList with filters
frappe.client.get
POSTGet full document
frappe.client.insert
POSTCreate document
frappe.client.save
POSTUpdate document
frappe.client.delete
POSTDelete document
frappe.client.submit
POSTSubmit document
frappe.client.cancel
POSTCancel document
frappe.client.get_count
POSTCount documents

Webhooks

Configure via Webhook DocType in the UI. Events:

EventTrigger
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.


HTTP Status Codes

CodeMeaningCommon Cause
200
Success
400
Bad requestValidation error
401
UnauthorizedMissing or invalid auth
403
ForbiddenNo permission for operation
404
Not foundDocument does not exist
417
Expectation failedServer exception (frappe.throw)
429
Rate limitedToo many requests
500
Server errorUnhandled exception

Critical Rules

  1. ALWAYS include
    Accept: application/json
    header in API requests
  2. ALWAYS add permission checks in
    @frappe.whitelist()
    methods
  3. ALWAYS validate and sanitize input in whitelisted methods
  4. ALWAYS use parameterized queries — NEVER string-interpolate SQL
  5. ALWAYS use
    timeout=30
    on external
    requests
    calls
  6. ALWAYS store credentials in
    frappe.conf
    or env vars — NEVER hardcode
  7. ALWAYS verify webhook signatures with HMAC-SHA256
  8. ALWAYS paginate list responses — NEVER return unbounded result sets
  9. NEVER use
    allow_guest=True
    on state-changing endpoints
  10. NEVER log credentials or sensitive data
  11. NEVER use Administrator API keys for integrations — create dedicated API users

Anti-Patterns

Do NOTDo 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 limitPaginate 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 formatALWAYS return
{"status": "...", "data": ...}

Version Differences

Featurev14v15v16
/api/resource/
(v1)
YesYesYes
/api/v2/document/
(v2)
NoYesYes
/api/v2/doctype/{dt}/meta
NoYesYes
/api/v2/doctype/{dt}/count
NoYesYes
limit
alias parameter
NoYesYes
PKCE for OAuth2LimitedYesYes
Server Script rate limitingNoYesYes
Doc method via v2 URLNoYesYes

Reference Files

FileContents
authentication-methods.mdToken, Session, OAuth2 with code examples
rest-api-reference.mdComplete REST CRUD with filters and pagination
rpc-api-reference.mdWhitelisted methods, frappe.call, frappe.xcall
webhooks-reference.mdWebhook config, security, handler examples
anti-patterns.mdCommon mistakes with fixes
examples.mdPython/JS/cURL client implementations

Related Skills

  • frappe-core-permissions
    — Permission system for API endpoints
  • frappe-core-database
    — Database queries behind API methods
  • frappe-syntax-hooks
    — Hook configuration for webhooks
  • frappe-syntax-controllers
    — Controller methods called via API

Verified against Frappe docs 2026-03-20 | Frappe v14/v15/v16