Hello again, tech enthusiasts! ๐
Do you type the same accounting entries into ERPNext every month? Bank fees, rent, small card payments. Each one takes two minutes. Fifty of them take a whole afternoon, and one typo can break your month-end.
Good news: ERPNext has a REST API. An API is like a waiter: you send a request, and it does the job in the kitchen for you. With a short Python script, you can create these entries from a spreadsheet, attach the receipt, and submit them.
Today we will do this the safe way: limited access, drafts first, and clear error checks.
What you need
- An ERPNext site, for example
https://erp.example.com - Permission to create users (System Manager)
- Python 3.9 or newer (https://www.python.org)
- The requests library:
pip install requests
Step 1: Create a user with limited rights
Do not use the Administrator account in scripts. If the key leaks, someone gets full control.
- Go to Users โ New and create a user like
api-bot@example.com. - Give it only the role Accounts User. This role can create and submit Journal Entries.
- Open the user, find the API Access section, and click Generate Keys.
- Copy the API Secret now. ERPNext shows it only once. The API Key stays visible on the user page.
Keep both values out of your code. Put them in environment variables. An environment variable is a value stored by your system, not in the file.
export ERP_URL="https://erp.example.com"
export ERP_API_KEY="YOUR_API_KEY"
export ERP_API_SECRET="YOUR_API_SECRET"
Step 2: Connect and test
ERPNext uses a header called Authorization with the word token, then the key and secret joined by a colon.
import os
import requests
BASE = os.environ["ERP_URL"]
session = requests.Session()
session.headers.update({
"Authorization": f"token {os.environ['ERP_API_KEY']}:{os.environ['ERP_API_SECRET']}",
"Accept": "application/json",
})
# Who am I? A quick test of the login
r = session.get(f"{BASE}/api/method/frappe.auth.get_logged_user", timeout=30)
r.raise_for_status()
print("Logged in as:", r.json()["message"])
Step 3: Build a balanced entry
A Journal Entry has rows. Each row uses one account and has a debit or a credit. The golden rule of accounting: total debits must equal total credits. If they do not match, ERPNext will refuse the entry.
Account names must be exact, including the company code at the end, like “Bank Charges – EC”. Copy them from your chart of accounts.
entry = {
"voucher_type": "Journal Entry",
"company": "Example Company",
"posting_date": "2025-12-05",
"user_remark": "Monthly bank fee, December",
"accounts": [
{"account": "Bank Charges - EC", "debit_in_account_currency": 25.00},
{"account": "Main Bank - EC", "credit_in_account_currency": 25.00},
],
}
# Check the balance before sending anything
debit = sum(row.get("debit_in_account_currency", 0) for row in entry["accounts"])
credit = sum(row.get("credit_in_account_currency", 0) for row in entry["accounts"])
assert round(debit, 2) == round(credit, 2), f"Not balanced: {debit} vs {credit}"
Step 4: Create it as a draft
We send the entry with a POST request to /api/resource/Journal Entry. The space in the name becomes %20 in the URL. A new document starts as a draft. A draft does not change your account balances yet, so it is safe.
def check(response):
"""Raise a clear error if ERPNext says no."""
if not response.ok:
try:
data = response.json()
msg = data.get("exception") or data.get("_server_messages") or data
except ValueError:
msg = response.text[:500]
raise RuntimeError(f"ERPNext error {response.status_code}: {msg}")
return response.json()
r = session.post(f"{BASE}/api/resource/Journal%20Entry", json=entry, timeout=30)
doc = check(r)["data"]
name = doc["name"]
print("Draft created:", name)
Now open ERPNext in your browser and look at the draft. For the first few runs, always check by eye.
Step 5: Attach the receipt
Every entry should have proof, like a PDF receipt. The upload_file method attaches a file to a document. We set is_private to 1, so only logged-in users with access can open it.
with open("receipts/bank_fee_dec.pdf", "rb") as f:
r = session.post(
f"{BASE}/api/method/upload_file",
files={"file": ("bank_fee_dec.pdf", f, "application/pdf")},
data={"doctype": "Journal Entry", "docname": name, "is_private": 1},
timeout=60,
)
print("Attached:", check(r)["message"]["file_url"])
Step 6: Submit it
When the draft looks right, submit it. Submitting means “make it final”. ERPNext then posts the amounts to the ledger. To submit, set docstatus to 1.
r = session.put(
f"{BASE}/api/resource/Journal%20Entry/{name}",
json={"docstatus": 1},
timeout=30,
)
submitted = check(r)["data"]
print("Submitted:", submitted["name"], "docstatus =", submitted["docstatus"]) # 1 = submitted
A submitted entry cannot be edited. To fix a mistake, cancel it in ERPNext and make a new one. This keeps an honest history.
Common problems
- 403 or “Not permitted” โ The API user is missing the Accounts User role, or the company is restricted with User Permissions.
- “Could not find Account” โ The account name is not exact. Add the company code, like ” – EC”.
- “Total Debit must be equal to Total Credit” โ Your rows do not balance. Round to 2 decimals before you send.
- “Cost Center is required” โ Some accounts need one. Add
"cost_center": "Main - EC"to the row, or set a default in the company settings.
Wrap-up
You now have a safe pattern: a limited user, secrets outside the code, a balance check, drafts first, a receipt attached, and then submit. Start with drafts only. When you trust the script, add the submit step.
Next idea: read your entries from an Excel file with pandas and loop over the rows, so a whole month of fees takes one click.