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:

  1. Identify the right journal
  2. Retrieve ledger accounts
  3. Post balanced accounting entries
  4. (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)

ScopeFeatures
ledger_entries:allCreate and manage accounting entries
(optional) file_attachments:allAttach 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:

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

PrefixDescription
401Supplier accounts
411Customer accounts
512Bank accounts
606–607Purchases / Expenses
706–707Revenues
💡

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 date and label
  • 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 CauseErrorHow to Fix
Invalid payload or missing field400 Bad RequestCheck JSON structure
Invalid or expired token401 UnauthorizedRefresh or verify token
Missing scope403 ForbiddenAdd ledger_entries:all
Totals not balanced422 Unprocessable EntityVerify debit = credit
Invalid journal or account ID404 Not FoundCheck 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.