Create Ledger Entries via API
Use this guide to create accounting (ledger) entries via the API and automatically register them in your Pennylane workspace.
Goal
This tutorial shows how to record custom accounting entries via API - for example, to handle payroll, manual adjustments, or external imports.
You will learn how to:
- Identify the right journal
- Retrieve ledger accounts
- Post balanced accounting entries
- (Optional) Attach supporting documents
End result:
A new ledger entry is created and visible in your Pennylane workspace, properly balanced and linked to the right accounts.
Who is this for?
Developers and partners building integrations that synchronize or automate accounting entries between external systems and Pennylane.
Authentication
Partner integrations must use OAuth 2.0 for authentication.
In sandbox environments, you can test with a Company API token, but OAuth 2.0 is required for production integrations published on the Marketplace.
Before You Get Started
Required scopes (Company API v2)
| Scope | Features |
|---|---|
ledger_entries:all | Create and manage accounting entries |
(optional) file_attachments:all | Attach supporting files to ledger entries |
You will need:
- A Pennylane company selected through OAuth consent
- Your OAuth access token (or a Company token in sandbox)
- A valid journal_id
- Valid ledger_account_id values
- Basic understanding of debit/credit accounting
IDs are company-specific
Journal and ledger account IDs differ for each company. Always retrieve them dynamically in your integration.
See also:
- Autnentication Overview
- Understand Scopes
Step 1 | Identifying the Journal
Each ledger entry must belong to a journal (sales, purchases, bank, or general).
curl --request GET \
--url 'https://app.pennylane.com/api/external/v2/journals' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'✅ Result:
{
"items": [
{ "code": "HA", "id": 123456, "label": "Achats" },
{ "code": "VT", "id": 123457, "label": "Ventes" },
{ "code": "BQ", "id": 123458, "label": "Banque" }
]
}Tip: Journal codes are company-specific. Use the code (VT, HA, BQ, etc.) to identify the correct journal for your entry.
Step 2 | Retrieving Ledger Accounts
Each line in a ledger entry must reference a valid ledger account.
Example: Get a bank account (512):
curl --request GET \
--url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"512"}]' \
-H 'Authorization: Bearer <ACCESS_TOKEN✅ Result:
{
"items": [
{
"id": 98765,
"number": "512000",
"label": "Banque",
"enabled": true
}
]
}Common account prefixes
Prefix Description 401 Supplier accounts 411 Customer accounts 512 Bank accounts 606–607 Purchases / Expenses 706–707 Revenues
Tip: Always use account IDs (
ledger_account_id), not numbers.Amount fields must be strings (e.g.
"120.00").
Choosing the right ledger_account_id
In Pennylane, the same ledger account number can exist under multiple IDs, depending on the VAT rate configuration.
Example: account 706000 may exist in several variants:
- 706000 - VAT 20%
- 706000 - VAT 10%
- 706000 - VAT 5.5%
- 706000 - Exempt
Each version has its own internal
id.👉 Always select the ID that matches the correct VAT rate for your entry.
To do this, query the ledger accounts and pick the item whose VAT configuration matches your use case.
See the Best Practice guide Understanding VAT Rates on Ledger Accounts
Step 3 | Creating the Ledger Entry
Each entry must contain:
- A
dateandlabel - The
journal_id - At least two lines - one debit and one credit
✅ Example use case:
Record a customer payment (debit 512 / credit 411)
Example Request
curl --request POST \
--url https://app.pennylane.com/api/external/v2/ledger_entries \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"date": "2025-11-05",
"label": "Customer payment - Invoice F2025-001",
"journal_id": 123456,
"ledger_entry_lines": [
{
"debit": "1000.00",
"credit": "0.00",
"ledger_account_id": 98765,
"label": "Bank payment received"
},
{
"debit": "0.00",
"credit": "1000.00",
"ledger_account_id": 87654,
"label": "Customer invoice settlement"
}
]
}'✅ Result
{
"id": 456789,
"label": "Customer payment - Invoice F2025-001",
"journal_id": 123456,
"status": "recorded"
}Tip: Amount field types
All numeric amounts (e.g. debit, credit) must be sent as strings, not numbers.
Important:
The sum of all debit amounts must equal the sum of all credit amounts.
If not, the API returns:
422 Unprocessable Entity — Entry lines are not balanced.
Step 4 | (Optional) Attach a Supporting Document
You can attach a PDF or other proof document to a ledger entry.
1. Upload the File
curl --request POST \
--url https://app.pennylane.com/api/external/v2/file_attachments \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: multipart/form-data" \
-F [email protected]✅ Result:
{
"id": 34567,
"filename": "payment-proof.pdf",
"status": "uploaded"
}
2. Linking It to Your Entry
curl --request POST \
--url https://app.pennylane.com/api/external/v2/ledger_entries \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"date": "2025-11-05",
"label": "Customer payment with proof",
"journal_id": 123456,
"file_attachment_id": 34567,
"ledger_entry_lines": [...]
}'
Step 5 | Handling Multi-Currency Entries
You can create entries in a different currency using the currency field.
{
"date": "2025-11-05",
"label": "USD Payment received",
"journal_id": 123456,
"currency": "USD",
"ledger_entry_lines": [...]
}Tip: Exchange rate defaults to 1.0 unless you specify another rate.
Step 6 | Tracking Changes
To monitor updates or synchronize accounting data, use the changelog endpoint:
curl --request GET \
--url 'https://app.pennylane.com/api/external/v2/changelogs/ledger_entry_lines?start_date=2025-01-01T00:00:00Z' \
-H "Authorization: Bearer <ACCESS_TOKEN>"✅ Result:
Returns entries created, updated, or deleted since the specified date.
Common Pitfalls
| Likely Cause | Error | How to Fix |
|---|---|---|
| Invalid payload or missing field | 400 Bad Request | Check JSON structure |
| Invalid or expired token | 401 Unauthorized | Refresh or verify token |
| Missing scope | 403 Forbidden | Add ledger_entries:all |
| Totals not balanced | 422 Unprocessable Entity | Verify debit = credit |
| Invalid journal or account ID | 404 Not Found | Check resource IDs |
Best Practices
- Validate before posting: ensure debit and credit totals match
- Store returned IDs: for reconciliation or synchronization
- Attach supporting files: for better audit traceability
- Use clear labels: to make entries easy to identify
- Ensure idempotency: avoid duplicate entries by reusing unique references
- Keep formats consistent: dates in
YYYY-MM-DD, amounts as strings
Tip: If you re-post the same entry payload, the API does not prevent duplicates. Handle idempotency in your integration logic.
Updated 21 days ago
