{"id":59,"date":"2025-12-07T21:10:00","date_gmt":"2025-12-07T18:10:00","guid":{"rendered":"https:\/\/fsaeed.blog\/?p=59"},"modified":"2026-10-31T00:18:51","modified_gmt":"2026-10-30T21:18:51","slug":"erpnext-journal-entry-api","status":"publish","type":"post","link":"https:\/\/fsaeed.blog\/index.php\/2025\/12\/07\/erpnext-journal-entry-api\/","title":{"rendered":"Posting Accounting Entries to ERPNext Through Its API"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Hello again, tech enthusiasts! \ud83d\udc4b<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Good news: ERPNext has a <strong>REST API<\/strong>. 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Today we will do this the safe way: limited access, drafts first, and clear error checks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What you need<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An ERPNext site, for example <code><a href=\"https:\/\/erp.example.com\">https:\/\/erp.example.com<\/a><\/code><\/li>\n\n\n<li>Permission to create users (System Manager)<\/li>\n\n\n<li>Python 3.9 or newer (<a href=\"https:\/\/www.python.org\">https:\/\/www.python.org<\/a>)<\/li>\n\n\n<li>The requests library: <code>pip install requests<\/code><\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Create a user with limited rights<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Do not use the Administrator account in scripts. If the key leaks, someone gets full control.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <strong>Users \u2192 New<\/strong> and create a user like <code>api-bot@example.com<\/code>.<\/li>\n\n\n<li>Give it only the role <strong>Accounts User<\/strong>. This role can create and submit Journal Entries.<\/li>\n\n\n<li>Open the user, find the <strong>API Access<\/strong> section, and click <strong>Generate Keys<\/strong>.<\/li>\n\n\n<li>Copy the <strong>API Secret<\/strong> now. ERPNext shows it only once. The <strong>API Key<\/strong> stays visible on the user page.<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export ERP_URL=\"https:\/\/erp.example.com\"\nexport ERP_API_KEY=\"YOUR_API_KEY\"\nexport ERP_API_SECRET=\"YOUR_API_SECRET\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Connect and test<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">ERPNext uses a header called <code>Authorization<\/code> with the word <code>token<\/code>, then the key and secret joined by a colon.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\nimport requests\n\nBASE = os.environ[\"ERP_URL\"]\nsession = requests.Session()\nsession.headers.update({\n    \"Authorization\": f\"token {os.environ['ERP_API_KEY']}:{os.environ['ERP_API_SECRET']}\",\n    \"Accept\": \"application\/json\",\n})\n\n# Who am I? A quick test of the login\nr = session.get(f\"{BASE}\/api\/method\/frappe.auth.get_logged_user\", timeout=30)\nr.raise_for_status()\nprint(\"Logged in as:\", r.json()[\"message\"])<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Build a balanced entry<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>Journal Entry<\/strong> has rows. Each row uses one account and has a <strong>debit<\/strong> or a <strong>credit<\/strong>. The golden rule of accounting: total debits must equal total credits. If they do not match, ERPNext will refuse the entry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Account names must be exact, including the company code at the end, like &#8220;Bank Charges &#8211; EC&#8221;. Copy them from your chart of accounts.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>entry = {\n    \"voucher_type\": \"Journal Entry\",\n    \"company\": \"Example Company\",\n    \"posting_date\": \"2025-12-05\",\n    \"user_remark\": \"Monthly bank fee, December\",\n    \"accounts\": [\n        {\"account\": \"Bank Charges - EC\", \"debit_in_account_currency\": 25.00},\n        {\"account\": \"Main Bank - EC\", \"credit_in_account_currency\": 25.00},\n    ],\n}\n\n# Check the balance before sending anything\ndebit = sum(row.get(\"debit_in_account_currency\", 0) for row in entry[\"accounts\"])\ncredit = sum(row.get(\"credit_in_account_currency\", 0) for row in entry[\"accounts\"])\nassert round(debit, 2) == round(credit, 2), f\"Not balanced: {debit} vs {credit}\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Create it as a draft<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We send the entry with a POST request to <code>\/api\/resource\/Journal Entry<\/code>. The space in the name becomes <code>%20<\/code> in the URL. A new document starts as a <strong>draft<\/strong>. A draft does not change your account balances yet, so it is safe.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def check(response):\n    \"\"\"Raise a clear error if ERPNext says no.\"\"\"\n    if not response.ok:\n        try:\n            data = response.json()\n            msg = data.get(\"exception\") or data.get(\"_server_messages\") or data\n        except ValueError:\n            msg = response.text[:500]\n        raise RuntimeError(f\"ERPNext error {response.status_code}: {msg}\")\n    return response.json()\n\nr = session.post(f\"{BASE}\/api\/resource\/Journal%20Entry\", json=entry, timeout=30)\ndoc = check(r)[\"data\"]\nname = doc[\"name\"]\nprint(\"Draft created:\", name)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now open ERPNext in your browser and look at the draft. For the first few runs, always check by eye.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Attach the receipt<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every entry should have proof, like a PDF receipt. The <code>upload_file<\/code> method attaches a file to a document. We set <code>is_private<\/code> to 1, so only logged-in users with access can open it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>with open(\"receipts\/bank_fee_dec.pdf\", \"rb\") as f:\n    r = session.post(\n        f\"{BASE}\/api\/method\/upload_file\",\n        files={\"file\": (\"bank_fee_dec.pdf\", f, \"application\/pdf\")},\n        data={\"doctype\": \"Journal Entry\", \"docname\": name, \"is_private\": 1},\n        timeout=60,\n    )\nprint(\"Attached:\", check(r)[\"message\"][\"file_url\"])<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 6: Submit it<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When the draft looks right, submit it. Submitting means &#8220;make it final&#8221;. ERPNext then posts the amounts to the ledger. To submit, set <code>docstatus<\/code> to 1.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>r = session.put(\n    f\"{BASE}\/api\/resource\/Journal%20Entry\/{name}\",\n    json={\"docstatus\": 1},\n    timeout=30,\n)\nsubmitted = check(r)[\"data\"]\nprint(\"Submitted:\", submitted[\"name\"], \"docstatus =\", submitted[\"docstatus\"])  # 1 = submitted<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A submitted entry cannot be edited. To fix a mistake, cancel it in ERPNext and make a new one. This keeps an honest history.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Common problems<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>403 or &#8220;Not permitted&#8221;<\/strong> \u2192 The API user is missing the Accounts User role, or the company is restricted with User Permissions.<\/li>\n\n\n<li><strong>&#8220;Could not find Account&#8221;<\/strong> \u2192 The account name is not exact. Add the company code, like &#8221; &#8211; EC&#8221;.<\/li>\n\n\n<li><strong>&#8220;Total Debit must be equal to Total Credit&#8221;<\/strong> \u2192 Your rows do not balance. Round to 2 decimals before you send.<\/li>\n\n\n<li><strong>&#8220;Cost Center is required&#8221;<\/strong> \u2192 Some accounts need one. Add <code>\"cost_center\": \"Main - EC\"<\/code> to the row, or set a default in the company settings.<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Wrap-up<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to create, attach files to, and submit Journal Entries in ERPNext from a safe Python script.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[49,47,7,48,12],"class_list":["post-59","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-accounting","tag-api","tag-automation","tag-erpnext","tag-python"],"_links":{"self":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/59","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/comments?post=59"}],"version-history":[{"count":1,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/59\/revisions"}],"predecessor-version":[{"id":74,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/59\/revisions\/74"}],"wp:attachment":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/media?parent=59"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/categories?post=59"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/tags?post=59"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}