Every ERPNext site already has a full REST API. Integrations with a mobile app, an e-commerce shop or a bank feed usually need no new endpoints at all, just the right credentials and URLs. When they do need something custom, a whitelisted Python method in your app is the standard way.

Authenticate with a token

For server-to-server calls, use API keys:

  1. Create a dedicated user for the integration, such as shop-integration@yourcompany.co.ke, with only the roles it needs.
  2. Open that user, go to the API Access section and use Generate Keys. Only a System Manager can do this. Store the secret straight away in your integration's secret store.
  3. Send both on every request:
curl -s "https://erp.example.com/api/resource/Customer/CUST-00042" \
  -H "Authorization: token :"

The request runs as that user, with that user's roles and User Permissions. A dedicated, limited user means a leaked key can only do what that integration needed.

Documents through /api/resource

/api/resource/ maps to standard operations:

  • GET /api/resource/Sales Order - list records. Returns name only and 20 rows unless you ask for more.
  • GET /api/resource/Sales Order/ - one full document.
  • POST /api/resource/Sales Order - create, with the document as JSON in the body.
  • PUT /api/resource/Sales Order/ - update the fields you send.
  • DELETE /api/resource/Sales Order/ - delete.

Lists take fields, filters, order_by and limit_page_length as query parameters, with JSON values:

curl -s -G "https://erp.example.com/api/resource/Customer" \
  -H "Authorization: token :" \
  --data-urlencode 'fields=["name","customer_name","customer_group"]' \
  --data-urlencode 'filters=[["customer_group","=","Commercial"]]' \
  --data-urlencode 'limit_page_length=100'

Results come back as {"data": [...]}. Permissions are checked the same way as in the desk.

v16 also serves a newer API under /api/v2/, with routes such as /api/v2/document/. The original routes keep working, so check which one your client library expects.

Custom endpoints with frappe.whitelist

When the standard routes are not enough, write a function in your app and mark it with @frappe.whitelist():

import frappe


@frappe.whitelist(methods=["GET"])
def open_invoices(customer: str):
    frappe.has_permission("Customer", "read", doc=customer, throw=True)
    return frappe.get_list(
        "Sales Invoice",
        filters={"customer": customer, "docstatus": 1, "outstanding_amount": [">", 0]},
        fields=["name", "posting_date", "due_date", "outstanding_amount"],
        order_by="due_date asc",
    )

If this lives in upeo_custom/api.py, it is called at:

GET /api/method/upeo_custom.api.open_invoices?customer=CUST-00042

and returns {"message": [...]}.

Rules for whitelisted methods

  • Check permissions yourself. @frappe.whitelist() only means "a logged-in user may call this". The function decides what they may see. frappe.get_list applies the caller's permissions; frappe.get_all does not, so do not return get_all results to callers.
  • Limit the HTTP method. By default a whitelisted function accepts GET, POST, PUT and DELETE. Pass methods=["GET"] for reads and methods=["POST"] for anything that changes data.
  • Be careful with allow_guest=True. It opens the method to anyone on the internet. Validate every argument and never return private data.
  • Use type hints. They document the API and let the framework check argument types.
  • Return plain data. Lists and dictionaries serialise cleanly.

Try it

  1. On a test site, create an integration user with the Sales User role and generate keys.
  2. Fetch one customer with curl using the token header.
  3. Add the open_invoices method to your app and call it.
  4. Remove the user's read access to Customer and call it again. It should be refused.

Designing APIs that are safe to expose is a core part of ERPNext as a Platform.