Import a Customer Invoice via API

Use this guide to import customer (sales) invoices into Pennylane and automatically register them in your invoicing and accounting workspace.

Goal

This tutorial shows how to automatically import customer invoices into Pennylane from your own system - for example, a billing tool, CRM, or ERP - so they appear in your accounting workspace, linked to the right customer and ready for reconciliation.

You will learn how to:

  1. Upload a customer invoice file (PDF)
  2. Import the invoice in Pennylane via API
  3. (Optional) Categorize the invoice for reporting

End result:

Your customer invoice appears in your Pennylane workspace, linked to the right customer, with correct VAT, amounts, and categories — ready for accounting.

👥

Who is this for?

Developers and partners building integrations that sync customer invoices into Pennylane automatically.

🔒

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
customer_invoices:allCreate or import customer invoices
file_attachments:allUpload and attach invoice PDFs
(optional) products:readonlyRetrieve products for invoice lines
(optional) ledger_accounts:readonlyRetrieve account IDs for proper accounting mapping
(optional) categories:allCategorize invoices for reporting

You will need:

  • A Pennylane company selected through OAuth consent
  • Your OAuth access token (or a Company token in sandbox)
  • A valid customer_id (existing or newly created)
  • A valid invoice file (PDF) to attach
  • (Optional) ledger account IDs - e.g., 706/707 for Revenue, 4457 for Output VAT, 530 for Cash, or 511… for payment providers
  • (Optional) product IDs if you sync invoice lines from your product catalog
ℹ️

IDs are company-specific

Customer, product, and ledger account IDs differ for each company.

Store these IDs per tenant in your integration to avoid mismatches.

📘

See also:

Step 1 | (Optional) Verifying Authentication

Before importing invoices, 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 setting up your integration for the first time, or when switching between sandbox and production environments.

Step 2 | Uploading the Invoice File

Before importing your invoice, upload the PDF file to Pennylane.

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

The API returns a JSON object containing an id.

Use this file_attachment_id in the next step to import your invoice.

Example response

{
  "id": 4321,
  "filename": "invoice-october.pdf",
  "status": "uploaded"
}
💡

Tip: Accepted file format & size Only PDF files are supported for customer invoice imports. The /file_attachments endpoint accepts files up to 100 MB.

Uploading another file format for an invoice will return a validation error.

Step 3 | Importing the Customer Invoice

Now that your invoice file is uploaded, use the import endpoint to create the corresponding invoice in Pennylane.

curl --request POST \
  --url https://app.pennylane.com/api/external/v2/customer_invoices/import \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "file_attachment_id": 4321,
    "customer_id": 1001,
    "date": "2025-10-01",
    "deadline": "2025-10-31",
    "currency_amount_before_tax": "100.00",
    "currency_tax": "20.00",
    "currency_amount": "120.00",
    "invoice_lines": [
      {
        "ledger_account_id": 706002,
        "currency_amount": "120.00",
        "currency_tax": "20.00",
        "quantity": 2,
        "raw_currency_unit_price": "50.00",
        "unit": "piece",
        "vat_rate": "FR_200"
      }
    ]
  }'

Result

A successful request creates the invoice in your Pennylane workspace and returns its id and main details.

Example response

{
  "id": 5678,
  "status": "imported",
  "currency_amount": "120.00",
  "customer_name": "ABC Consulting"
}
⚠️

Important:

The sum of all invoice line currency_amount values must equal the total currency_amount.

Otherwise, the API returns 422 Unprocessable Entity — Entry lines are not balanced.

💡

Ledger Account Mapping

If you omit the ledger_account_id, Pennylane automatically applies your company’s default mapping, based on its chart of accounts and VAT settings.

You can override this by specifying the account ID explicitly in your payload.

📘

VAT Rate Codes Reference

The vat_rate field must use one of the supported VAT rate codes.

CodeDescriptionRate
FR_200Standard VAT France20%
FR_100Reduced VAT France10%
FR_055Reduced VAT France5.5%
exemptExempt (0%)0%
anyNo specific VAT code

For the full list of supported VAT codes, see the API Reference for the Create a customer invoice endpoint.

Step 4 | Validating the Import

After importing the invoice, verify that it was successfully created in Pennylane.

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

Result

A successful response confirms the invoice exists and was imported correctly.

Example response

{
  "id": 5678,
  "status": "imported",
  "currency_amount": "120.00",
  "customer_name": "ABC Consulting"
}
⚠️

Common errors

  • 401 Unauthorized → Invalid or expired OAuth token.
  • 403 Forbidden → Missing scope (customer_invoices:all).
  • 422 Unprocessable Entity → Totals mismatch or missing required fields.
💡

Tip: In sandbox mode, you can test safely using dummy customers and sample amounts.
Always verify that your currency_amount and line totals are consistent before moving to production.

Step 5 | (Optional) Categorizing the Invoice

Categorization helps you analyze your sales by product line, department, or business activity.

You can assign one or several categories to a customer invoice.

1. Get or Create a Category

# List existing categories
GET /api/external/v2/categories

# Create a new category
POST /api/external/v2/categories

2. Assign the Category to the Invoice

curl --request PUT \
  --url https://app.pennylane.com/api/external/v2/customer_invoices/{invoice_id}/categories \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '[
    { "id": 123, "weight": 1.0 }
  ]'
📘

Note:

The weight value represents the percentage allocation:

1.0 = 100%, 0.5 = 50%. You can assign multiple categories with different weights

When to Use /import vs /create

Both endpoints create customer invoices, but they serve different workflows:

ActionEndpointWhen to use
Uploading and attaching a PDF file/customer_invoices/importWhen your invoice is already generated by another system (e.g., POS, ERP, or billing software)
Generating invoices directly/customer_invoicesWhen you want to create and send invoices programmatically from your own app
💡

Tip: Use /import when you already have a PDF.

Use /create when you need Pennylane to build the invoice from structured data.

Test in Sandbox or Postman

You can test this flow inyour Pennylane Sandbox or with your own Postman setup..

  • Collection: Pennylane API v2 → Customer Invoices → Import
  • Replace <ACCESS_TOKEN> with your sandbox token
  • Use real customer_id and ledger_account_id from your sandbox company

Result

Testing in sandbox ensures your flow works end-to-end before deploying to production.

Common Pitfalls

IssueLikely CauseHow to Fix
400 Bad RequestInvalid payload — unexpected field or missing required propertyCheck field names, required fields, and data types
401 UnauthorizedMissing or expired OAuth tokenRefresh token or check header format
403 ForbiddenMissing scope (customer_invoices:all)Add missing scope to your OAuth app
404 File not foundUploaded file expired or deletedRe-upload using /file_attachments
422 Entry lines are not balancedTotals mismatch between HT / TVA / TTCRecalculate totals
Duplicate invoicesRe-importing the same payloadImplement deduplication logic (e.g., store file_attachment_id or external reference)
💡

Tip: If you re-import the same file, the API does not automatically deduplicate entries - handle this in your integration logic.

💡

Tip: A 400 Bad Request is often returned when a field name is misspelled, missing, or not supported by the endpoint.

For example, including "label" or an extra field not expected in /customer_invoices/import will trigger this error.

Best Practices

  • Validate before import — Check totals and VAT consistency client-side.
  • Match to the right accounts — e.g., 706xxx for sales, 445xxx for VAT.
  • Ensure idempotency — Use a stable label or external reference to prevent duplicates.
  • Secure tokens — Rotate and store OAuth tokens safely.
  • Test in sandbox first — Validate the flow before running it in production.
💡

Idempotency - Avoiding duplicates

Pennylane does not enforce idempotency for invoice imports.

To prevent duplicate invoices, your integration should handle deduplication client-side.

We recommend using a stable unique identifier, such as:

  • the file_attachment_id, or
  • a combination of (customer/supplier_id, invoice_number, date).

How Pennylane Handles Accounting

When you import a customer invoice:

  • Pennylane automatically creates revenue entries in your accounting ledgers.
  • The entries appear in your Sales journal.
  • The invoice is visible in Invoicing → Customer Invoices in your workspace.
📘

Note

Entries are generated based on your company’s accounting scheme and the ledger_account_id values provided in your payload.