Create a Customer Invoice via API

Use this endpoint when you want Pennylane to generate the invoice from structured data, rather than importing an existing PDF.

Goal

This tutorial shows how to generate customer invoices directly from your system — for example, from a CRM, billing app, or ERP — without importing an existing PDF.

You will learn how to:

  1. Create a new customer invoice from structured data
  2. Add products, VAT, and accounting mappings
  3. (Optional) Send or categorize the invoice for reporting

End Result:

Your customer invoice is created and appears in your Pennylane workspace, linked to the right customer and ready for accounting and reconciliation.

👥

Who is this for?

Developers and partners building integrations that generate and sync invoices directly in Pennylane.

🔒

Authentication

Partner integrations must use OAuth 2.0 for authentication.

In sandbox environments, you can test this flow using 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 and send customer invoices
(optional)* transactions:readonly`Retrieve payment or reconciliation data
(optional) products:readonlyRetrieve product details
(optional) ledger_accounts:readonlyFetch accounting mappings
(optional) categories:allCategorize invoices
(optional) file_attachments:allAttach files to invoices

You will need:

  • A Pennylane company selected through OAuth consent
  • Your OAuth access token (or Company token in sandbox)
  • A valid customer_id (existing or newly created)
  • (Optional) product IDs if your integration uses a product catalog
  • (Optional) ledger account IDs — e.g., 706 or 707 for revenue, 4457 for VAT
ℹ️

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 creating 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 | (Optional) Preparing References

Before creating the invoice, make sure your integration can identify or create the necessary entities.

Find or Create a Customer

Check if the customer exists:

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

If not, create a company customer:

curl --request POST \
  --url 'https://app.pennylane.com/api/external/v2/company_customers' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "ACME Corp",
    "emails": ["[email protected]"],
    "external_reference": "ACME_001",
    "billing_address": {
      "address": "123 Business St",
      "postal_code": "75001",
      "city": "Paris",
      "country": "FR"
    }
  }'

Result

Keep the returned customer_id — you’ll need it to create the invoice.

(Optional) Retrieve Products

If your invoice references products:

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

Tip: Using products ensures consistent pricing and VAT handling across invoices.

(Optional) Retrieve Ledger Accounts

For precise accounting, retrieve available accounts:

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>'
📘

Common ledger accounts

  • 706xxx → Service revenue
  • 707xxx → Product sales

Step 3 | Creating the Customer Invoice

Once references are ready, create the invoice directly in Pennylane.

Minimal Example

curl --request POST \
  --url "https://app.pennylane.com/api/external/v2/customer_invoices" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": 123,
    "date": "2025-10-01",
    "deadline": "2025-10-31",
    "invoice_lines": [
      {
        "label": "Consulting services",
        "quantity": 2,
        "unit": "hour",
        "raw_currency_unit_price": "100.00",
        "vat_rate": "FR_200"
      }
    ],
    "external_reference": "INV-ACME-2025-001"
  }'

Result

This creates a finalized invoice with one line item.

💡

Tip: Amount field types

All numeric amounts (e.g. currency_amount, currency_tax, currency_amount_before_tax) must be provided as strings, not numbers.

Invoice with Product and Template

curl --request POST \
  --url "https://app.pennylane.com/api/external/v2/customer_invoices" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": 123,
    "date": "2025-10-01",
    "deadline": "2025-10-31",
    "customer_invoice_template_id": 456,
    "invoice_lines": [
      {
        "product_id": 789,
        "quantity": 3,
        "unit": "piece",
        "raw_currency_unit_price": "50.00",
        "vat_rate": "FR_200"
      }
    ],
    "draft": true
  }'
💡

Tip: Use "draft": true to create a draft invoice; omit it (or set false) to finalize immediately.

Example Response

{
  "id": 9876,
  "invoice_number": "INV-2025-001",
  "status": "draft",
  "currency_amount_before_tax": "150.00",
  "currency_tax": "30.00",
  "currency_amount": "180.00",
  "customer": { "id": 123 },
}
💡

Ledger Account Mapping

If you omit the ledger_account_id, Pennylane automatically applies the company’s default mapping (chart of accounts & VAT settings).

You can override this by specifying a ledger_account_id explicitly.

📘

VAT Rate Codes Reference

vat_rate must use a supported code.

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 Create a Customer Invoice.

Step 4 | Verifying the Invoice

Check the created invoice:

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

Result

{
  "id": 9876,
  "status": "draft",
  "currency_amount": "180.00",
  "customer": "ACME Corp"
}
⚠️

Common errors

  • 400 Bad Request → Missing or invalid field
  • 401 Unauthorized → Invalid token
  • 403 Forbidden → Missing customer_invoices:all scope
  • 422 Unprocessable Entity → Totals mismatch or invalid VAT code

Step 5 | Optional Actions

1. Send the Invoice by Email

curl --request POST \
  --url "https://app.pennylane.com/api/external/v2/customer_invoices/9876/send_by_email" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "recipients": ["[email protected]"] }'

2. Categorize the Invoice

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

Note

weight represents the allocation percentage (1.0 = 100%, 0.5 = 50%). Multiple categories are supported.

3. Track Reconciliation

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

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 → Create
  • Replace <ACCESS_TOKEN> with your sandbox token
  • Use real customer_id (and optional product_id) from your sandbox company

Result

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

Common Pitfalls

ErrorLikely CauseHow to Fix
400 Bad RequestInvalid payload or missing required propertyCheck field names and formats
401 UnauthorizedMissing or expired tokenRefresh or verify header
403 ForbiddenMissing scopeAdd customer_invoices:all
404 Not FoundInvalid customer_id or endpointCheck IDs and URL
422 Unprocessable EntityTotals or VAT mismatchRecalculate amounts

Best Practices

  • Validate before creation - check VAT and totals consistency client-side.
  • Use accurate accounts - e.g., 706 or 707 for sales
  • Secure tokens - rotate and store them safely
  • Test in sandbox first - before running in production