Connect Your Point of Sale (POS) to Pennylane

Use this guide to connect your Point of Sale (POS) system to Pennylane and automatically post your daily sales reports (Ticket Z) as balanced accounting entries.

Goal

This tutorial shows how to integrate a POS with Pennylane to send daily sales summaries as accounting entries so your revenue, VAT, and payment data are automatically synchronized.

You will learn how to:

  1. Verify authentication and permissions
  2. Identify or create a POS journal
  3. Map your sales, VAT, and payment accounts
  4. Post daily entries to Pennylane

End result:

Your POS automatically creates daily sales entries in Pennylane — balanced, traceable, and ready for accounting.

👥

Who is this for?

Integration partners or software vendors building a POS > Pennylane integration.

🔒

Authentication

Partner integrations must use OAuth 2.0 for authentication.

In sandbox environments, you can test this flow 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
ledgercreate entries, list journals & ledger accounts
file_attachments:alloptional – attach Ticket Z PDFs

You will need:

  • A Pennylane company selected through OAuth consent
  • Your OAuth access token
  • Company-specific IDs (journals, ledger accounts) you’ll fetch below
ℹ️

IDs are company-specific

Journal and ledger account IDs differ per company. Store them per tenant in your POS.

Step 1 | (Optional) Verify Authentication

Before posting sales entries, confirm that your token and environment are correctly set up.

curl https://app.pennylane.com/api/external/v2/me \
-H "Authorization: Bearer <ACCESS_TOKEN>"

✅ Expected response: 200 OK - authentication successful.

💡

Tip: Run this check when configuring your POS integration for the first time, or when switching between sandbox and production environments.

Step 2 | Identify (or Creating) the POS Journal

List Journals (Let the User Choose)

curl --request GET \
  --url https://app.pennylane.com/api/external/v2/journals \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Typical codes

CodeJournal
VTSales journal
HAPurchases journal
BQBank journal
ODGeneral journal

Result

Store the chosen journal_id and reuse it for all POS entries.

Create a Dedicated POS Journal (if Missing)

curl --request POST \
  --url https://app.pennylane.com/api/external/v2/journals \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "POS01",
    "label": "POS Cash Journal"
  }'

Result

Keep the returned id → your POS journal_id.

Step 3 | Prepare Ledger Accounts (Revenue, VAT, Payment Types)

💡

Tip: Most companies already have these accounts.

If not, guide the user to create or confirm them once, then store the IDs in your POS mapping.

Revenue (Net Sales)

Revenue accounts typically start with 706 (services) or 707 (goods).

curl --request GET \
  --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"706"}]' \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
💡

Tip: Keep one revenue account per VAT rate (e.g., 7061 = 10%, 7062 = 20%) for easier reporting.

If missing, create one:

curl --request POST \
  --url https://app.pennylane.com/api/external/v2/ledger_accounts \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "7062",
    "label": "Sales – VAT 20%"

VAT (Output VAT)

📘

About VAT rate codes

When posting entries that include VAT, make sure to use the correct VAT ledger account (usually 4457…) and, when required, the corresponding vat_rate code such as FR_200 (20%) or FR_100 (10%).

For details and examples, see the Understanding VAT Rates on Ledger Accounts page.

VAT accounts often start with 4457…

curl --request GET \
  --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"4457"}]' \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
📘

Note: These accounts usually exist already - avoid duplicates.

Payment Types

Payment method accounts often use 511xxx (payment intermediaries) or 530 (cash).

# Cash (530…)
curl --request GET \
  --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"53"}]' \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

# Payment providers (511…)
curl --request GET \
  --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"511"}]' \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

If missing, create one:

curl --request POST \
  --url https://app.pennylane.com/api/external/v2/ledger_accounts \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "511008",
    "label": "Uber Eats Clearing"
  }'

Result:

You now have your mapping:

  • Revenue per VAT rate → ledger_account_id
  • VAT collected per VAT rate → ledger_account_id
  • Payment methods (cash, card, providers) → ledger_account_id

Store this mapping in your POS for reuse.

Step 4 | Handle Rounding Differences (required)

Ticket Z totals must be balanced:

Sum(Credits) = Revenue + VAT; Sum(Debits) = Payments.

Small rounding gaps happen. Add a balancing line:

  • If Credit < Debit → add Credit line to account 758 (miscellaneous income)
  • If Debit < Credit → add Debit line to account 658 (miscellaneous expense)

Fetch the account ID(s) if needed:

curl --request GET \
  --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"658"}]' \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Step 5 | (Optional) Attach the Ticket Z PDF 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:

Keep the returned id to associate later with your ledger entry.

Step 6 | Post the Ticket Z as a Ledger Entry

Build one balanced entry per day (or per shift).

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-01-01",
    "label": "Ticket Z – Main Register – 2025-01-01",
    "journal_id": <YOUR_JOURNAL_ID>,
    "ledger_entry_lines": [
      { "debit": "0.00",  "credit": "90.91", "ledger_account_id": <YOUR_REVENUE_ACCOUNT_ID> },
      { "debit": "0.00",  "credit": "9.09",  "ledger_account_id": <YOUR_VAT_ACCOUNT_ID> },
      { "debit": "100.00","credit": "0.00",  "ledger_account_id": <YOUR_PAYMENT_ACCOUNT_ID> }
    ]
  }'

Result: 201 Created - entry successfully posted.

💡

Tip: Amounts must be strings ("100.00") and the entry must be perfectly balanced.

Step 7 | Validate & Troubleshoot

Common errors

  • 401 Unauthorized → Check OAuth token or header format.
  • 403 Forbidden → Missing scope (ledger), or wrong company context.
  • 422 Entry lines are not balanced → Recalculate totals or add a rounding line.

Validation checklist (per company)

  • POS journal selected and stored (journal_id)
  • Revenue, VAT, and payment accounts mapped to IDs
  • Ticket Z totals computed per VAT rate and payment type
  • Entry lines balanced; rounding handled via 658 / 758

Verifying your entries

curl --request GET \
  --url 'https://app.pennylane.com/api/external/v2/ledger_entries?filter=[{"field":"date","operator":"eq","value":"2025-01-03"},{"field":"journal_id","operator":"eq","value":<YOUR_JOURNAL_ID>}]' \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
📘

Note:

GET /ledger_entries/{id} isn’t available in all environments.

Use the list endpoint or check directly in the UI.

🔍

Allowed filters;

Fields → updated_at, created_at, date, journal_id

Operators → eq, not_eq, lt, gt, lteq, gteq

Best Practices

  • One mapping step per company — confirm journal & accounts once, then reuse.
  • One entry per business cycle — daily (Ticket Z) or per shift, be consistent.
  • Attach artifacts — add the Ticket Z PDF for auditability.
  • Ensure idempotency — reuse a stable label or reference to avoid duplicates.
  • Observe limits — keep entries concise and balanced.

✅ Following these practices ensures stable, auditable sales posting and cleaner books.