Most people who start using ERPNext eventually hit a point where they need it to talk to something else.
Maybe it’s a website that needs to pull product prices. Maybe it’s a mobile app for the warehouse team. Maybe it’s a reporting dashboard that wants live data without someone exporting a CSV every morning. Whatever the use case, the answer is the same: the ERPNext REST API.
The good news is that ERPNext ships with a fully functional REST API out of the box. No plugins, no extra configuration, no separate authentication server. Every doctype you create, every field you add, every custom script you write — they all get API endpoints automatically.
The less good news is that the official documentation tells you what the endpoints are but not really how to use them in real situations. If you’ve read the docs and still feel unsure about where to start, this guide fills in those gaps.
First, What Exactly Is the REST API?
REST means Representational State Transfer. That’s a fancy way of saying: you send a normal HTTP request to a URL, and you get back a JSON response. If you’ve ever used a web API before, this will feel familiar.
ERPNext exposes two types of endpoints:
- Document endpoints for CRUD operations on doctypes.
/api/resource/Sales Order/SO-2024-0001gives you one sales order./api/resource/Sales Orderwith a POST request creates one. - Method endpoints for calling specific Python functions.
/api/method/frappe.client.get_listlets you query multiple records with filters.
The base URL is always https://your-instance.com/api. Everything starts from there.
Authentication: Tokens, Not Sessions
You have two practical options for authenticating API requests. Both work. Which one you pick depends on what you’re building.
Option 1: Token-based authentication (recommended for most cases)
Every user in ERPNext can generate an API key and API secret from their user settings. Go to My Settings, find the API Access section, and generate a token. You get two strings: an api_key and an api_secret.
To use them, include both as headers in every request:
Authorization: token your-api-key:your-api-secretNo session management. No cookies. No CSRF tokens to worry about. This is the way to go for server-to-server communication, scripts, and backend integrations.
Option 2: OAuth 2.0 (for when you need to act on behalf of different users)
If you’re building something where multiple users log in and the API needs to respect their individual permissions, OAuth is the better choice. ERPNext supports the authorization code grant flow. You register a client in the OAuth Client doctype, get a client ID and secret, and redirect users through the authorization screen.
For most integrations though, a simple API token tied to a service user is cleaner. Create a dedicated user like “api_integration” with the exact roles it needs and nothing more. Generate its API token. Use that token everywhere.
Your First Working Request
Let’s start with something simple. You want to fetch a specific sales order.
curl -X GET \
https://your-instance.erpnext.space/api/resource/Sales%20Order/SO-2024-0001 \
-H "Authorization: token abc123:xyz789"The response is a JSON object with the entire document. Every field, every child table row, timestamps, owner info — the whole thing.
A few things to notice right away. The doctype name in the URL is URL-encoded, so “Sales Order” becomes “Sales%20Order”. Case matters. “Sales Order” works. “sales order” doesn’t.
Also, the document name after the doctype is the full name as it appears in ERPNext, not the database ID. If your sales orders are named SO-2024-0001, that’s what goes in the URL.
Listing Documents With Filters
Getting one record is straightforward. Getting a filtered list is where most real work happens.
ERPNext provides a method called frappe.client.get_list that handles queries. Here’s what a filtered request looks like:
curl -X POST \
https://your-instance.erpnext.space/api/method/frappe.client.get_list \
-H "Authorization: token abc123:xyz789" \
-H "Content-Type: application/json" \
-d '{
"doctype": "Sales Order",
"fields": ["name", "customer", "grand_total", "status"],
"filters": [["status", "=", "Draft"]],
"limit_page_length": 50
}'This returns all draft sales orders, limited to 50 results, showing only the four fields you asked for.
The filters format takes some getting used to. It’s a list of lists. Each inner list is [field, operator, value]. Common operators: =, !=, >, <, >=, <=, like, in, not in, between.
Multiple filters AND together automatically. If you want OR logic, it gets a bit more involved. You wrap conditions in an outer list with an or operator:
json
"filters": [
["status", "=", "Draft"],
["docstatus", "=", 1]
]This gives you documents where status is Draft AND docstatus is 1. For OR, the syntax is less intuitive:
json
"filters": [
["status", "in", ["Draft", "Open"]]
]Which reads as: status is in the list containing Draft and Open. Equivalent to status = Draft OR status = Open.
Creating and Updating Records
Creating a new record is a POST request to the resource endpoint with the field values in the body.
bash
curl -X POST \
https://your-instance.erpnext.space/api/resource/Sales%20Order \
-H "Authorization: token abc123:xyz789" \
-H "Content-Type: application/json" \
-d '{
"customer": "Acme Corp",
"transaction_date": "2025-01-15",
"delivery_date": "2025-01-22",
"items": [
{
"item_code": "PROD-001",
"qty": 5,
"rate": 120.00
}
]
}'If it works, you get back a JSON object with the new document’s name and other metadata. If it doesn’t, the error message usually tells you which field has a problem. Missing mandatory fields are the most common issue. Check the doctype’s field definitions if you’re unsure what’s required.
Updating is similar but you include the document name in the URL:
bash
curl -X PUT \
https://your-instance.erpnext.space/api/resource/Sales%20Order/SO-2025-00045 \
-H "Authorization: token abc123:xyz789" \
-H "Content-Type: application/json" \
-d '{
"delivery_date": "2025-01-29"
}'This updates only the delivery date and leaves everything else untouched.
Child Tables: The Tricky Part
The items table in a sales order is a child table. Child tables in ERPNext work differently from regular fields and this trips up a lot of developers.
When you update a document that has a child table, you must include the full child table data in your request. Not just the row you want to change — all of them. If you send only one item row in an update, the other rows get deleted.
The safest pattern is:
- Fetch the current document with a GET request.
- Modify the items array in your code (add a row, remove a row, change a value).
- Send the entire modified items array back in your PUT request.
This ensures nothing gets lost. It’s a bit more work but it prevents data disasters.
Calling Custom Methods
The API isn’t limited to CRUD operations. You can call any whitelisted Python method in your ERPNext instance.
Let’s say you have a server-side script that generates a monthly sales report. The method is called generate_sales_report and lives in a file under your custom app. You expose it to the API by adding @frappe.whitelist() above the function definition.
Then you call it:
bash
curl -X POST \
https://your-instance.erpnext.space/api/method/your_app.your_module.generate_sales_report \
-H "Authorization: token abc123:xyz789" \
-H "Content-Type: application/json" \
-d '{
"month": "January",
"year": 2025
}'Anything the function returns becomes the JSON response. This is how you build custom endpoints that do things no standard doctype operation can handle: complex calculations, multi-document workflows, external API integrations, custom exports.
The method must be whitelisted. If you forget the decorator, the API returns a permission error. It’s a security measure that prevents arbitrary code execution through the API layer.
Handling Errors Gracefully
The API returns standard HTTP status codes. A 200 means success. A 400 means your request was malformed. A 403 means you don’t have permission. A 404 means the resource doesn’t exist. A 417 means something failed validation on the server side — think missing mandatory field or a value that doesn’t pass the field’s validation rules.
The response body for errors always includes a message and sometimes a traceback. In production, you want to log these but never expose the traceback to end users. The message alone usually explains what went wrong.
A common mistake is hitting rate limits. ERPNext doesn’t have aggressive rate limiting by default, but if your integration makes hundreds of requests per minute, you might start seeing connection errors. Add some delay between requests if you’re processing bulk data.
Real-World Pattern: Building a Stock Check Endpoint
Here’s a concrete example that ties everything together. Suppose your warehouse team uses a mobile app that needs to check stock levels quickly. They scan a barcode and want to know how many units are available.
Rather than making them navigate the full ERPNext interface, you build a simple API endpoint. Create a Python method in your custom app:
python
import frappe
@frappe.whitelist()
def check_stock(item_code, warehouse=None):
if warehouse:
bin_data = frappe.get_all("Bin",
filters={"item_code": item_code, "warehouse": warehouse},
fields=["actual_qty", "reserved_qty", "warehouse"])
else:
bin_data = frappe.get_all("Bin",
filters={"item_code": item_code},
fields=["actual_qty", "reserved_qty", "warehouse"])
return bin_dataNow the mobile app calls:
bash
curl -X POST \
https://your-instance.erpnext.space/api/method/your_app.inventory.check_stock \
-H "Authorization: token abc123:xyz789" \
-H "Content-Type: application/json" \
-d '{"item_code": "PROD-001", "warehouse": "Main Store"}'And gets back a clean JSON response with actual quantity, reserved quantity, and warehouse name. The app displays it instantly. No screen navigation, no filters to set, no training required.
This pattern — a small, focused custom method behind the API — is how you make ERPNext feel like it was purpose-built for your operation.
A Few Things That Will Save You Time
Always use a dedicated service user for integrations. Don’t use your personal account or the Administrator account. Create a separate user with a name like “api_integration” and assign only the roles it needs. When someone leaves the company, their API token doesn’t take down your integrations with them.
Test with Postman or HTTPie first. Before writing any code, make sure your requests work in a tool where you can see the full response. It makes debugging much faster than trying to decipher error messages from inside your application code.
Respect the permissions system. The API enforces the same role-based permissions as the web interface. If the api_integration user doesn’t have permission to read Sales Orders, the API returns a 403. Assign roles carefully. Granting too many permissions is a security risk but too few and nothing works.
Field names are case-sensitive and use underscores. In the UI you see “Grand Total” but in the API it’s grand_total. “Delivery Date” is delivery_date. Check the doctype definition or do a GET request on an existing document to see the exact field names.
Child tables have a fieldname prefix when referenced from the parent. The items table in a Sales Order has the fieldname items. Each row inside it has fields like item_code, qty, rate. But if you’re filtering on a child table field from the parent level, the syntax changes. This is an advanced topic but it’s worth knowing it exists before you spend an hour wondering why your filter isn’t matching.
When to Use the API vs When to Build Inside ERPNext
The API is powerful but it’s not always the right tool. If what you’re building interacts with users through a screen, consider building it as a Frappe app with a Desk page instead. You get authentication, permissions, and the UI framework for free.
The API shines when:
- You need an external system to push or pull data (e-commerce site, payment gateway, logistics provider)
- You need a mobile-friendly lightweight interface that doesn’t need the full ERPNext Desk
- You’re automating processes with scripts, cron jobs, or workflow tools like n8n
- You’re building dashboards or reports in external BI tools
If your use case is internal users doing daily work, the native ERPNext interface is almost always the better experience.
Where to Go Next
Once you’re comfortable with basic API operations, the next topics worth exploring are:
- Webhooks in ERPNext, which push data to external URLs when documents change
- Server-side scripts that extend the API with custom business logic
- The Frappe client library for Python, which wraps the REST API in a cleaner syntax
- Rate limiting and performance tuning for high-volume integrations
The API is the bridge between ERPNext and everything else your business runs. Learn it well enough and you stop thinking of ERPNext as a closed system. It becomes the core your other tools connect to.
We run managed ERPNext hosting at erpnext.space. If you’re building integrations and need a hosting environment that handles the infrastructure while you focus on the API, reach out. We handle the server so you can handle the code.