mcpField
F
frappe-core-api

frappe-core-api

github.com/impertio-studioDevelopment198124 installsUpdated 30w ago
Repository

About frappe-core-api

A guide to REST, RPC, and webhook integration patterns for Frappe developers, covering API authentication, document CRUD, custom methods, and event notifications. It also addresses version differences and security practices such as pagination, permission checks, and signature verification.

Key features

  1. 01REST document CRUD and versioned endpoints
  2. 02Token, OAuth 2.0, and session authentication
  3. 03Custom method calls through RPC
  4. 04Webhook events and signature verification
  5. 05Pagination, permissions, and input validation

Use cases

  1. 01Read and write Frappe documents from an external client
  2. 02Configure authentication for an integration
  3. 03Call custom server logic through RPC
  4. 04Receive and verify document-change webhooks

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-core-api

After installing, type/frappe-core-apiin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
anti-patterns.md
authentication-methods.md
examples.md
rest-api-reference.md
rpc-api-reference.md
webhooks-reference.md

Details

From the project README

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

List Parameters
ParameterTypeDescriptionDefault
fieldsJSON arrayFields to return["name"]
filtersJSON arrayAND conditionsnone
or_filtersJSON arrayOR conditionsnone
order_bystringSort expressionmodified desc
limit_startintPagination offset0
limit_page_lengthintPage size20
limitintAlias for limit_page_length [v15+]—
debugboolShow SQL in responsefalse
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=TrueNo authentication neededAll
methods=["POST"]Restrict HTTP methods[v14+]
xss_safe=TrueSkip 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_valuePOSTGet single field value
frappe.client.get_listPOSTList with filters
frappe.client.getPOSTGet full document
frappe.client.insertPOSTCreate document
frappe.client.savePOSTUpdate document
frappe.client.deletePOSTDelete document
frappe.client.submitPOSTSubmit document
frappe.client.cancelPOSTCancel document
frappe.client.get_countPOSTCount documents

Webhooks

Configure via Webhook DocType in the UI. Events:

EventTrigger
after_insertNew document created
on_updateEvery save
on_submitAfter submit (docstatus=1)
on_cancelAfter cancel (docstatus=2)
on_trashBefore delete
on_update_after_submitAfter amendment
on_changeOn 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
200Success—
400Bad requestValidation error
401UnauthorizedMissing or invalid auth
403ForbiddenNo permission for operation
404Not foundDocument does not exist
417Expectation failedServer exception (frappe.throw)
429Rate limitedToo many requests
500Server 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 whitelistfrappe.has_permission() before action
frappe.db.sql(f"...{user_input}")Parameterized %s queries
allow_guest=True + state changeRequire authentication
Return all records without limitPaginate with limit_page_length
Hardcode API credentialsfrappe.conf.get("api_key")
Synchronous heavy processingfrappe.enqueue() for long tasks
No timeout on external callsrequests.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}/metaNoYesYes
/api/v2/doctype/{dt}/countNoYesYes
limit alias parameterNoYesYes
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

Comments

Markdown supported