# Table of Contents - [API Overview](#api-overview) - [Choose the Right API for Your Integration](#choose-the-right-api-for-your-integration) - [Introduction to the Pennylane API](#introduction-to-the-pennylane-api) - [Create a Firm API Token](#create-a-firm-api-token) - [Data Sharing or API: Which Should You Use?](#data-sharing-or-api-which-should-you-use-) - [Accounting Reporting 📊](#accounting-reporting-) - [Create a Company API Token](#create-a-company-api-token) - [Supported Use Cases](#supported-use-cases) - [Implement OAuth 2.0](#implement-oauth-2-0) - [Understand Scopes](#understand-scopes) - [Error Handling & Status Codes](#error-handling-status-codes) - [Create Ledger Entries via API](#create-ledger-entries-via-api) - [Connect Your Point of Sale (POS) to Pennylane](#connect-your-point-of-sale-pos-to-pennylane) - [Filter API Data](#filter-api-data) - [Import a Supplier Invoice via API](#import-a-supplier-invoice-via-api) - [Import a Customer Invoice via API](#import-a-customer-invoice-via-api) - [Filter Ledger Entries](#filter-ledger-entries) - [Filter Supplier Invoices](#filter-supplier-invoices) - [Create a Customer Invoice via API](#create-a-customer-invoice-via-api) - [Filter Ledger Accounts](#filter-ledger-accounts) - [Filter Suppliers](#filter-suppliers) - [Filter Draft Invoices](#filter-draft-invoices) - [Filter Products](#filter-products) - [Filter Credit Notes](#filter-credit-notes) - [Filter Categories](#filter-categories) - [Filter Ledger Entry Lines](#filter-ledger-entry-lines) - [Filter Customers](#filter-customers) - [Use Cursor-Based Pagination](#use-cursor-based-pagination) - [Create a Downpayment Invoice](#create-a-downpayment-invoice) - [Public Roadmap](#public-roadmap) - [Automate Payment Matching](#automate-payment-matching) - [Rate Limiting in API v2](#rate-limiting-in-api-v2) - [Track Data Changes with the API](#track-data-changes-with-the-api) - [How to reach out to us](#how-to-reach-out-to-us) - [API Changelog](#api-changelog) - [Import vs Create a Customer Invoice](#import-vs-create-a-customer-invoice) - [Glossary](#glossary) - [Understanding VAT Rates on Ledger Accounts](#understanding-vat-rates-on-ledger-accounts) - [Invoicing Management](#invoicing-management) - [String Character Limits](#string-character-limits) - [Send Documents by Email](#send-documents-by-email) - [Payment vs Matched Transaction](#payment-vs-matched-transaction) - [Filter Customer Invoices](#filter-customer-invoices) - [Migrate from API v1 to v2](#migrate-from-api-v1-to-v2) - [V1 Scopes](#v1-scopes) - [V1 Rate limiting](#v1-rate-limiting) - [Testing my connection](#testing-my-connection) - [Sending estimates / invoices by email](#sending-estimates-invoices-by-email) - [Filter my customers](#filter-my-customers) - [Filter my invoices](#filter-my-invoices) - [V1 - Setting up filters](#v1-setting-up-filters) - [Filter my suppliers](#filter-my-suppliers) - [How to reach out to us](#how-to-reach-out-to-us) - [Filter my billing subscriptions](#filter-my-billing-subscriptions) - [Filter my estimates](#filter-my-estimates) - [Automating payment matching](#automating-payment-matching) - [API Versioning](#api-versioning) --- # API Overview Welcome to the **Pennylane API !** This section gives you the foundations you need to connect, authenticate, and start building integrations with Pennylane. With the API, you can access and sync invoicing, accounting, banking, and financial data or automate entire workflows for your company, firm, or integration partner use cases. > 👥 > > **Who is this for?** > > Developers, accounting firms, and integration partners building workflows or applications on top of Pennylane. Start with the essentials: * [Introduction to the Pennylane API](https://pennylane.readme.io/docs/getting-started) : What the API does, who it’s for, and how it’s organized. * [Choose the Right API for Your Integration](https://pennylane.readme.io/docs/what-apis-are-available) : Understand Company tokens, Firm tokens, and OAuth 2.0 flows. * **Authentication Overview:** How to authenticate and manage access securely. * **Create & Configure Your Sandbox:** Set up a safe environment to test your API calls. > 💡 > > Use these guides to get started, then explore domain-specific tutorials (invoicing, ledger entries, banking, etc.). Updated about 1 month ago * * * Ask AI --- # Choose the Right API for Your Integration Before You Get Started [](https://pennylane.readme.io/docs/what-apis-are-available#before-you-get-started) ============================================================================================================== Pennylane provides different APIs depending on your role and integration needs. Each API offers specific features, authentication methods, and access conditions. This guide explains when and why to use each API, what they are best for, and how to choose the right one for your use case. API Overview [](https://pennylane.readme.io/docs/what-apis-are-available#api-overview) ========================================================================================== | API | Target Users | Authentication | Main Features | Access | | --- | --- | --- | --- | --- | | **Company API** | Companies, Integration partners | Company API Token or OAuth 2.0 | **\>** Manage sales (invoices, customers, products) **\>** Manage purchases (supplier invoices, suppliers) **\>** Create and retrieve accounting entries **\>** Manage analytical tags **\>** Retrieve accounting reports (trial balance, FEC, ledger, etc.) | Default for all accounts with an Essential plan or higher | | **Firm API** | Accounting firms, Integration partners | Firm API Token or Firm OAuth | **\>** Access your portfolio of client companies **\>** Retrieve company list **\>** Retrieve accounting reports (trial balance, FEC, analytical ledger, etc.) **\>** Create and retrieve accounting entries | Default for firms | | **Firm Group API** | Accounting firms (advanced use cases) | Firm Group Token or OAuth 2.0 | **\>** Create companies in the firm context **\>** Create users in the firm context | **On request** via Support or your Account Manager | Choosing the Right API [](https://pennylane.readme.io/docs/what-apis-are-available#choosing-the-right-api) ============================================================================================================== * **Choose the [Company API](https://pennylane.readme.io/docs/*https://pennylane.readme.io/v2.0/reference/versioning) ** if you are company, using Pennylane or an integration partner building features for companies, and you need to automate financial and accounting operations (e.g., invoice generation, bookkeeping). * **Choose the [Firm API](https://firm-pennylane.readme.io/reference/scopes) ** if you are an accounting firm managing multiple client companies, or an integration partner building solutions for firms, and you need consolidated access to client data and reports. * **Choose the [Firm Group API](https://pennylane-firm-group.readme.io/reference/getcompanies#:~:text=AD%20MU%20NO-,saas_plan,-string) ** only if you require advanced management features such as creating companies or users in bulk within the firm context. Updated about 1 month ago * * * * [Create a Company API Token](https://pennylane.readme.io/docs/generating-my-api-token) * [Create a Firm API Token](https://pennylane.readme.io/docs/firm-api) * [Implement OAuth 2.0](https://pennylane.readme.io/docs/oauth-20-walkthrough) Ask AI --- # Introduction to the Pennylane API Before Your Get Started [](https://pennylane.readme.io/docs/getting-started#before-your-get-started) ======================================================================================================== This guide will help you make your first API request with Pennylane, whether you are a company, an accounting firm, or an integrator. Before you start, please be aware that by using this API, you agree to be bound by our [API contract terms](https://pennylane.readme.io/page/api-contract-terms) . Choosing Your Integration Path [](https://pennylane.readme.io/docs/getting-started#choosing-your-integration-path) ====================================================================================================================== We offer several ways to connect to our API, depending on your role: | Role | Authentication Method | Sandbox Creation | | --- | --- | --- | | **Company** | Company API Token | Create sandbox in your Pennylane account | | **Firm (Accounting Practice)** | Firm API Token | Use your firm account (no sandbox required) | | **Integration Partner** | OAuth 2.0 | Request sandbox from Partnerships team | Authentication [](https://pennylane.readme.io/docs/getting-started#authentication) ====================================================================================== To call the Pennylane API, you must include a valid token in every request: Shell curl \\ -H "Authorization: Bearer " * **Companies**: generate a Company API token in your Pennylane account settings. * **Firms**: generate a Firm API token in your Firm account settings. * **Integrators**: obtain an OAuth 2.0 access token. Contact [\[email protected\]](https://pennylane.readme.io/cdn-cgi/l/email-protection#6a1a0b181e040f181902031a192a1a0f040413060b040f44090507) to get started. Creating Your Sandbox Environment [](https://pennylane.readme.io/docs/getting-started#creating-your-sandbox-environment) ============================================================================================================================ Companies [](https://pennylane.readme.io/docs/getting-started#companies) ---------------------------------------------------------------------------- To create your sandbox environment: **(1)** Log in to your Pennylane account. **(2)** Click your profile (top-right corner). **(3)** Select **Test environment**. **(4)** Click **Create my sandbox**. > 👍 > > You will now have two accounts: your live account and a sandbox account named `Sandbox – [your\[[email protected]](https://pennylane.readme.io/cdn-cgi/l/email-protection) ](mailto:[[email protected]](https://pennylane.readme.io/cdn-cgi/l/email-protection) ).` Firms (Accounting Practices) [](https://pennylane.readme.io/docs/getting-started#firms-accounting-practices) ---------------------------------------------------------------------------------------------------------------- Firms use their existing **Firm account** to access the API. No sandbox creation is required. Integrators [](https://pennylane.readme.io/docs/getting-started#integrators) -------------------------------------------------------------------------------- Email [\[email protected\]](https://pennylane.readme.io/cdn-cgi/l/email-protection#9bebfae9eff5fee9e8f3f2ebe8dbebfef5f5e2f7faf5feb5f8f4f6) with your full name and the email address to use for sandbox creation. Setting Up Your Account for Testing [](https://pennylane.readme.io/docs/getting-started#setting-up-your-account-for-testing) ================================================================================================================================ Depending on which parts of the API you want to test, you may need to configure your account in the Pennylane app. For invoice testing: **(1)** Go to your **Invoice settings** page. **(2)** Set up the following elements: * Company address & contact information * Bank account details * Logo **(3)** Configure invoice numbering (required by law: sequential numbering). > 📘 > > For further information, see [Step-by-step guide](https://help.pennylane.com/fr/articles/18863-parametrer-l-editeur-de-factures-clients) > . Verifying Your Setup [](https://pennylane.readme.io/docs/getting-started#verifying-your-setup) ================================================================================================== Once you have obtained your API token (or OAuth access token), you can verify that your setup is correct by calling the /me endpoint. This request confirms that your authentication works and that your environment (sandbox or production) is accessible. Shell curl https://app.pennylane.com/api/external/v2/me \ -H "Authorization: Bearer " **Expected JSON response** JSON { "id": "12345", "email": "[email protected]", "role": "customer" } Support [](https://pennylane.readme.io/docs/getting-started#support) ======================================================================== **Need help? We are here for you.** * **Companies** **\>** Contact Pennylane [Support](https://help.pennylane.com/fr/articles/22349-contacter-l-equipe-support-de-pennylane) * **Partners** **\>** Email [](https://pennylane.readme.io/cdn-cgi/l/email-protection#fd8d9c8f8993988f8e95948d8ebd8d98939384919c9398d39e9290) or use your Zendesk access ![](https://files.readme.io/6a07df7-image_3.png) Updated about 2 months ago * * * * [Generate a Company API Token](https://pennylane.readme.io/docs/generating-my-api-token) * [Firm API](https://pennylane.readme.io/docs/firm-api) * [Implement OAuth 2.0](https://pennylane.readme.io/docs/oauth-20-walkthrough) Ask AI --- # Create a Firm API Token Pennylane offers a firm API for the need of accounting firms and softwares that work with accounting firms. Here is the [API Reference](https://firm-pennylane.readme.io/) . > 📘 > > ### > > Access to the firm API > > [](https://pennylane.readme.io/docs/firm-api#access-to-the-firm-api) > > It is accessible through a Firm token, that is available in the settings of your accounting firm, vs the Company API that is accessible through a Company token, available in the settings of a company. Generating a Firm API token [](https://pennylane.readme.io/docs/firm-api#generating-a-firm-api-token) ========================================================================================================= Pennylane’s Firm API uses bearer tokens to authenticate requests. You can view and manage your token in the Pennylane firm dashboard. About Firm tokens [](https://pennylane.readme.io/docs/firm-api#about-firm-tokens) ------------------------------------------------------------------------------------- A token can only be linked to one firm. But a firm can be associated to several tokens and each token can grant access to different API scopes. As a consequence, you’ll need to create several tokens if: * You want to grant different levels of access to different users for the same firm. You’ll then need to create a new token every time you want to grand access to a new combination of scopes. * You’re using the Firm API with several firms of yours. You’ll then need to create one token per firm, at least. Generate your Firm token [](https://pennylane.readme.io/docs/firm-api#generate-your-firm-token) --------------------------------------------------------------------------------------------------- To generate your Firm API token: 1. Log into your Pennylane firm account. 2. Navigate to **Firm settings** > **Firm Tokens**. 3. Click **Generate an API Token**. 4. Give the token a **name** and select the **scopes** you want the token to give access to as well as its **expiration date**. 5. Click **Generate token**. 6. Copy your token and save it somewhere safe. For security purposes, tokens aren’t stored on your Pennylane workspace, so this is your only chance to copy yours. 7. Check the box confirming that you have saved your Firm API token elsewhere and click **Continue**. From there, you can either: * Generate another token for the same firm. * See existing tokens: name, scopes, creation date and creator. * Delete existing tokens. Updated 2 months ago * * * Ask AI --- # Data Sharing or API: Which Should You Use? Before You Get Started [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#before-you-get-started) ============================================================================================================================= Pennylane offers two main ways to connect and retrieve your data: **Data Sharing** and the **Pennylane API**. This guide explains when and why to use each option, what it’s best for, and how to decide between them - or combine both. > 📘 > > **Related documentation:** > > * [Full Data Sharing documentation](https://data-sharing.pennylane.com/docs/fr/getting_started.html) > > * [Getting Started With the Pennylane API](https://pennylane.readme.io/docs/getting-started) > | | **Data Sharing** | **API** | | --- | --- | --- | | **Mode** | Read-only | Read/Write | | **Freshness** | Few hours delay | Real-time | | **Volume** | Very large datasets | Small-to-medium datasets | | **Setup** | Easy with BI tools | Requires developer integration | | **Best for** | Analyzing data | Building custom integrations | > 👓 > > ### > > At a Glance > > [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#at-a-glance) > > * Use the **API** for **real-time, two-way integration** (read + write) with your own tools. > * Some teams use a **hybrid approach**: Data Sharing for bulk scheduled retrieval, and the API for live actions (e.g. creating invoices or updating customer records). Data Sharing [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#data-sharing) ========================================================================================================= For **data practitioners** (analysts, BI teams) who want to integrate Pennylane data into a data warehouse or BI environment. Key Characteristics [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#key-characteristics) ----------------------------------------------------------------------------------------------------------------------- * Read-only connection * Few hours delay * Optimized for very large datasets * Flexible querying * Easy setup with BI tools (Power BI, Tableau, Snowflake, …) Examples [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#examples) ------------------------------------------------------------------------------------------------- * Build a business activity dashboard in Power BI * Import Pennylane data into a data lake for predictive analytics API [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#api) ======================================================================================= For **software professionals** (developers, integration partners) building workflows with Pennylane. Key Characteristics [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#key-characteristics-1) ------------------------------------------------------------------------------------------------------------------------- * Read/write connection * Data as fresh as you query it * Efficient for small-to-medium datasets * Parameterized endpoints for precise queries * Requires developer setup Examples [](https://pennylane.readme.io/docs/should-i-use-data-sharing-or-the-api-1#examples-1) --------------------------------------------------------------------------------------------------- * Upload invoices from your invoicing tool * Send custom email notifications when invoices are overdue > 👉 > > Still unsure? Start with your **primary need**: > > * **Analytics at scale** → Data Sharing > * **Automation & integration** → API > * **Both** → Combine the two Updated about 1 month ago * * * * [Introduction to the Pennylane API](https://pennylane.readme.io/docs/getting-started) * [Choose the Right API for Your Integration](https://pennylane.readme.io/docs/what-apis-are-available) Ask AI --- # Accounting Reporting 📊 To establish accounting reporting automation with our API, here is the preferred user experience 👇 1. Perform a first data extract, using our exports [endpoints](https://pennylane.readme.io/v2.0/reference/exportanalyticalgeneralledger) or [datasharing](https://data-sharing.pennylane.com/docs/fr/getting_started.html) 2. You can then rely on our [changelog](https://pennylane.readme.io/v2.0/reference/getledgerentrylinechanges) to retrieve only the accounting lines that have been modified since your extraction. You can filter by date. 3. The changelog gives you the id of the ledger entry line that was modified. You can retrieve it [here](https://pennylane.readme.io/v2.0/reference/getledgerentryline) . > ❗️ > > ### > > It is not recommended to fetch all ledger entry lines on a regular basis, as it could cause time outs in the case your company has a high volume of ledger entries. Please use the data exports to retrieve all entries and then rely on the changelog to retrieve only the differential data. > > [](https://pennylane.readme.io/docs/accounting-reporting#it-is-not-recommended-to-fetch-all-ledger-entry-lines-on-a-regular-basis-as-it-could-cause-time-outs-in-the-case-your-company-has-a-high-volume-of-ledger-entries-please-use-the-data-exports-to-retrieve-all-entries-and-then-rely-on-the-changelog-to-retrieve-only-the-differential-data) Updated 3 months ago * * * Ask AI --- # Create a Company API Token Pennylane’s Company API uses **bearer tokens** to authenticate requests. Each token identifies your company and grants access to specific resources through defined **scopes** (permissions). Use this guide to **create, manage, and securely use your Company API tokens** when integrating your own tools with Pennylane. Before You Get Started [](https://pennylane.readme.io/docs/generating-my-api-token#before-you-get-started) ============================================================================================================== > 👉 > > To create and manage API tokens, you must: > > * Have an Essential plan or higher, > * Have admin access to the company workspace - available to Executive, Internal Accountant, and External Accountant roles. About Company API Tokens [](https://pennylane.readme.io/docs/generating-my-api-token#about-company-api-tokens) ================================================================================================================== Before creating your first token, here’s what you need to know about how Company API tokens work. * A **token is always linked to one company**. * A **company can have multiple tokens**, each with its own scopes and expiration date. In practice, you might want to create several tokens when: * You need to grant **different access levels** to separate users or integrations. * You manage **multiple companies** in Pennylane - each company requires its own token. Generating a Token [](https://pennylane.readme.io/docs/generating-my-api-token#generating-a-token) ====================================================================================================== To generate a Company API token: **(1)** Log in to your Pennylane Firm account. **(2)** In the **Accounting** section, go to **Settings > Connectivity > Developers**. **(3)** Click **Generate an API Token**. **(4)** Enter a **Token Name**. > 💡 > > **Tip:** Name your token after its purpose (e.g., “CRM sync” or “Invoice automation”). **(5)** Under **API V2**, choose the permissions: * **Read only** — retrieve data * **Read and write** — create or update data **(6)** Choose an **expiration date**: 1 month, 6 months, 12 months, or Unlimited. **(7)** Click **Generate Token**. **(8)** Copy your token immediately\*\* — it will only be displayed once. **(9)** Confirm that you have saved it securely, then click **Continue**. > ❗️ > > Tokens are not stored in Pennylane and cannot be retrieved later. If lost, you must generate a new one. > 🚧 > > ### > > Troubleshooting > > [](https://pennylane.readme.io/docs/generating-my-api-token#troubleshooting) > > If you don’t see the **Developers** tab in your company settings: > > * 🔒 You may not have the right permissions **\>** check with your **company admin** (only Executives and Accountants can access this tab). > * 💼 Your company may be on a **Starter plan** **\>** you’ll need to **upgrade to an Essential plan** or higher. Managing and Revoking Tokens [](https://pennylane.readme.io/docs/generating-my-api-token#managing-and-revoking-tokens) ========================================================================================================================== From the **Developers** tab, you can view and manage all existing tokens: * Token name * Scopes granted * Creation and expiration dates * Last used To revoke a token, click **Delete** next to it. Once deleted, it immediately loses access to the API. > ❗️ > > Token deletion is irreversible. Any integration using that token will stop working until a new one is generated and configured in its place. Using Multiple Tokens [](https://pennylane.readme.io/docs/generating-my-api-token#using-multiple-tokens) ============================================================================================================ Each token is **independent** and applies only its own scopes and expiration settings. If several tokens exist, the API enforces **the scopes of the token used in the request**. > Example: > > * **Token A**: read-only access > * **Token B**: read-and-write access > > **\>** Using **Token A** for a write request returns `403 Forbidden`. > 💡 > > **Tip:** It is recommended to create separate tokens for each app or environment to isolate permissions and reduce risk. Using Your Token [](https://pennylane.readme.io/docs/generating-my-api-token#using-your-token) ================================================================================================== Once your Company API token has been generated, you can use it to authenticate your API requests. Include your token in the `Authorization` header of every API request. cURL curl https://app.pennylane.com/api/external/v2/me \ -H "Authorization: Bearer " ### Example JSON Response [](https://pennylane.readme.io/docs/generating-my-api-token#example-json-response) JSON { "user": { "id": 479693, "first_name": "Paul", "last_name": "SMITH", "email": "[email protected]", "locale": "en" }, "company": { "id": 172916, "name": "My Company Name", "reg_no": "sandbox-172916" } } This response confirms that: * Your token is **valid**. * The API correctly identifies both your **user** and your **company context**. If you receive an error: * `401 Unauthorized` **\>** The token is missing or invalid. * `403 Forbidden` **\>** The token doesn’t include the required scope. * `404 Not Found` **\>** The endpoint doesn’t exist (check your base URL). Best Practices [](https://pennylane.readme.io/docs/generating-my-api-token#best-practices) ============================================================================================== ✅ Store tokens securely (e.g., secrets manager). ✅ Rotate tokens periodically. ✅ Never share tokens publicly or in version control. ✅ Revoke unused tokens to minimize risk. > 📘 > > For a full list of available permissions, see the [Understanding V2 Scopes](https://pennylane.readme.io/docs/v2-scopes#/) > . Updated about 1 month ago * * * * [Introduction to the Pennylane API](https://pennylane.readme.io/docs/getting-started) * [Implement OAuth 2.0](https://pennylane.readme.io/docs/oauth-20-walkthrough) * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) Ask AI --- # Supported Use Cases This page summarizes all business use cases currently supported by the Pennylane API. Use it as a functional overview to identify what you can build, and to navigate toward the relevant guides or API reference. > 📘 > > **Note:** New capabilities are now added in API v2, while v1 is deprecated and will not receive new features. Invoicing (Sales & Purchases) [](https://pennylane.readme.io/docs/supported-use-cases#invoicing-sales--purchases) ===================================================================================================================== | **Use Case** | Endpoint/Tutorial | **Documentation** | | --- | --- | --- | | Create a customer invoice | Tutorial | [Create a Customer Invoice via API](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case) | | Import a customer invoice (from PDF) | Tutorial | [Import a Customer Invoice via API](https://pennylane.readme.io/docs/customer-invoicing) | | Create a customer invoice from template | `/customer_invoices` | [API Reference](https://pennylane.readme.io/reference/postcustomerinvoices) | | Import a Supplier Invoice via API | Tutorial | [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/supplier-invoicing) | | Link a credit note to a customer invoice | `/credit_notes` | [API Reference](https://pennylane.readme.io/reference/linkcreditnote) | | Create invoices with analytical tags | \`\` | API Reference | | Handle imputation dates (PCA / CCA) | | API Reference | > 📘 > > **Note**: Importing invoices (customer or supplier) requires sending a PDF via /file\_attachments before using the /import endpoints. Accounting & Ledger [](https://pennylane.readme.io/docs/supported-use-cases#accounting--ledger) =================================================================================================== | **Use Case** | **Endpoint/Tutorial** | **Documentation** | | --- | --- | --- | | Create ledger entries | Tutorial | [Create a Ledger Entry via API](https://pennylane.readme.io/docs/create-a-ledger-entry)
T | | Create ledger entries with analytical tags | Tutorial | [Create Ledger Entries via API](https://pennylane.readme.io/docs/create-a-ledger-entry) | | Handle FNP (Accrued expenses) | | API Reference | | Handle FAE (Accrued income) | | API Reference | | Import a purchase order | `/purchase_orders` | [API Reference](https://pennylane.readme.io/reference/createpurchaserequestimport) | | Create banking transactions | `/transactions` | [API Reference](https://pennylane.readme.io/reference/createtransaction) | | Retrieve banking transactions | `/transactions/{id}` | [API Reference](https://pennylane.readme.io/reference/gettransaction) | > 💡 > > **Tip:** All ledger entries must be balanced: debit total = credit total. Documents & Attachments [](https://pennylane.readme.io/docs/supported-use-cases#documents--attachments) =========================================================================================================== | **Use Case** | **Endpoint/Tutorial** | Documentation | | --- | --- | --- | | Upload PDF documents | `/file_attachments` | [API Reference](https://pennylane.readme.io/reference/postfileattachments#/) | | Import Factur-X | | API Reference | | Attach files to invoices or ledger entries | `/file_attachments` | [API Reference](https://pennylane.readme.io/reference/postfileattachments#/) | Sales Workflow (Quotes & Subscriptions) [](https://pennylane.readme.io/docs/supported-use-cases#sales-workflow-quotes--subscriptions) ========================================================================================================================================= | **Use Case** | **Endpoint/Tutorial** | Documentation | | --- | --- | --- | | Create a quote | `/quotes` | [API Reference](https://pennylane.readme.io/reference/postquotes) | | Convert quote → customer invoice | `/customer_invoices/create_from_quote` | [API Reference](https://pennylane.readme.io/reference/createcustomerinvoicefromquote)
\` | | Manage billing subscriptions | | API Reference | Matching & Reconciliation [](https://pennylane.readme.io/docs/supported-use-cases#matching--reconciliation) =============================================================================================================== | **Use Case** | Endpoint/Tutorial | Documentation | | --- | --- | --- | | Match an invoice with a bank transaction | Feature Guide | [Automate Payment Matching](https://pennylane.readme.io/docs/automating-payment-matching) | | Retrieve matched transactions | `/customer_invoices/{id}/matched_transactions` | [API Reference](https://pennylane.readme.io/reference/getcustomerinvoicematchedtransactions) | What’s Not Supported Yet [](https://pennylane.readme.io/docs/supported-use-cases#whats-not-supported-yet) ============================================================================================================= These use cases are not available in the API: * ❌ Create a downpayment invoice * ❌ Create a delivery note If any of these are part of your workflow, please contact the Pennylane partnerships team for roadmap updates. Related Guides [](https://pennylane.readme.io/docs/supported-use-cases#related-guides) ========================================================================================== * Managing Your Invoicing * [Create a Customer Invoice via API](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case) * [Import a Customer Invoice via API](https://pennylane.readme.io/docs/customer-invoicing) * [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/supplier-invoicing) * Creating Ledger Entries * Handling VAT & Ledger Accounts * Tracking Data Changes (Changelog API) Updated about 2 months ago * * * * [Create a Customer Invoice via API](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case) * [Import a Customer Invoice via API](https://pennylane.readme.io/docs/customer-invoicing) * [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/supplier-invoicing) Ask AI --- # Implement OAuth 2.0 Pennylane supports OAuth 2.0, an industry-standard protocol that allows applications to request access to user data without requiring user credentials. This guide explains how to implement OAuth 2.0 with Pennylane: registering your app, authorizing users, exchanging and refreshing tokens, and revoking access when no longer needed. > ✍️ > > Use OAuth if you are: > > * An **integration partner** building a third-party app. > * An **accounting firm** accessing the Firm API with firm-level tokens. > 👉 > > Company customers should use API tokens instead (see [Generating a Company API Token](https://pennylane.readme.io/docs/generating-my-api-token) > ) OAuth 2.0 Flow Overview [](https://pennylane.readme.io/docs/oauth-20-walkthrough#oauth-20-flow-overview) ============================================================================================================ The process follows a standard Authorization Code flow: **(1)** User clicks Sign in with Pennylane in your app. **(2)** User approves access → Pennylane returns an authorization code. **(3)** Your app exchanges the code for an access token (and refresh token). **(4)** Use the access token in API calls. **(5)** Refresh the token when it expires. **(6)** Revoke the token when access is no longer needed. > 📘 > > For background reading, see [this document](https://datatracker.ietf.org/doc/html/rfc6749#section-1.2) > . Token Types & Resource Scope [](https://pennylane.readme.io/docs/oauth-20-walkthrough#token-types--resource-scope) ====================================================================================================================== Company Token [](https://pennylane.readme.io/docs/oauth-20-walkthrough#company-token) ----------------------------------------------------------------------------------------- Granted for a single company. Access is limited to that company’s resources, bounded by the requested/approved scopes (e.g., read/write products, invoices). Firm Token [](https://pennylane.readme.io/docs/oauth-20-walkthrough#firm-token) ----------------------------------------------------------------------------------- Granted at the firm level. Can access resources across multiple client companies managed by the firm. The exact list of companies (including confidential ones) depends on who granted access and their internal permissions. Scopes still apply, but visibility is filtered by the granting user’s access. > ✍️ > > **In short**: scopes define _what_ you can do, while the token context (company vs firm) and granting user define _which_ resources you can access. **Examples** * With a company token: GET /api/external/v2/products → products for that company only. * With a firm token: GET /api/external/v2/companies → companies visible to the granting firm user; subsequent calls must be made in the context of a selected company. Step 1 | Registering Your Application [](https://pennylane.readme.io/docs/oauth-20-walkthrough#step-1--registering-your-application) ======================================================================================================================================== Before using OAuth, register your app with Pennylane by contacting our Partnerships team. **(1)** Contact the **Partnerships team** via [this form](https://www.pennylane.com/fr/contact-demande-de-partenariat/) . **(2)** Once validated, you will receive a Client ID and Client Secret. > 🚧 > > The Client Secret must be kept secure and never shared. > > Store both Client ID and Client Secret immediately, as they cannot be retrieved later. If they are lost, a new OAuth app must be created, which may take time. Step 2 | Adding a “Sign in with Pennylane” Button [](https://pennylane.readme.io/docs/oauth-20-walkthrough#step-2--adding-a-sign-in-with-pennylane-button) ============================================================================================================================================================== Redirect users to: `https://app.pennylane.com/oauth/authorize` ### GET Parameters [](https://pennylane.readme.io/docs/oauth-20-walkthrough#get-parameters) | Parameter | Required | Description | | --- | --- | --- | | `client_id` | yes | ID provided when your app is registered | | `redirect_uri` | yes | URL where the user is redirected after approval | | `response_type` | yes | Must be `code` | | `scope` | yes | Space-separated list of scopes requested | | `state` | no | Unique string to prevent CSRF attacks | The user is prompted to select one of their available companies or firms and grant access. Step 3 | Exchanging Code for Access Token [](https://pennylane.readme.io/docs/oauth-20-walkthrough#step-3--exchanging-code-for-access-token) ================================================================================================================================================ After approval, Pennylane redirects to your redirect\_uri with a code (and state). Verify that the state matches what you sent to prevent tampering. Then exchange the code for tokens. Shell curl -X POST https://app.pennylane.com/oauth/token \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "code=AUTH_CODE" \ -d "redirect_uri=YOUR_REDIRECT_URI" \ -d "grant_type=authorization_code" ### Body Parameters [](https://pennylane.readme.io/docs/oauth-20-walkthrough#body-parameters) | Parameter | Required | Description | | --- | --- | --- | | `client_id` | yes | Your Client ID | | `client_secret` | yes | Your Client Secret | | `code` | yes | Authorization code received in Step 2 | | `redirect_uri` | yes | Must match the one used in Step 2 | | `grant_type` | yes | Must be `authorization_code` | ### Example response [](https://pennylane.readme.io/docs/oauth-20-walkthrough#example-response) JSON { "access_token": "abc123...", "token_type": "Bearer", "expires_in": 86400, "refresh_token": "def456..." } Step 4 | Refreshing the Token [](https://pennylane.readme.io/docs/oauth-20-walkthrough#step-4--refreshing-the-token) ======================================================================================================================== Access tokens expire (default: 24h). Use the refresh token to get a new one: Shell curl -X POST https://app.pennylane.com/oauth/token ### Body parameters [](https://pennylane.readme.io/docs/oauth-20-walkthrough#body-parameters-1) | Parameter | Required | Description | | --- | --- | --- | | `client_id` | yes | Your Client ID | | `client_secret` | yes | Your Client Secret | | `refresh_token` | yes | Refresh token received with the access token | | `grant_type` | yes | Must be `refresh_token` | > 📘 > > Refresh tokens are valid for **90 days**. Once used, a refresh token is revoked and replaced. Step 5 | Revoking the Token [](https://pennylane.readme.io/docs/oauth-20-walkthrough#step-5--revoking-the-token) ==================================================================================================================== When access is no longer needed, revoke the token: Shell POST https://app.pennylane.com/oauth/revoke ### Body parameters [](https://pennylane.readme.io/docs/oauth-20-walkthrough#body-parameters-2) | Parameter | Required | Description | | --- | --- | --- | | `client_id` | yes | Your Client ID | | `client_secret` | yes | Your Client Secret | | `token` | yes | The access or refresh token to revoke | Returns **HTTP 200 OK** with an empty body if successful. Best Practices [](https://pennylane.readme.io/docs/oauth-20-walkthrough#best-practices) =========================================================================================== ✅ Always validate the state parameter. ✅ Store token creation time and refresh before expiration. ✅ Request only the minimal scopes required (principle of least privilege). ✅ Keep your Client Secret secure. ✅ Encrypt and safely store access/refresh tokens (e.g., secrets manager). ✅ Revoke tokens when no longer needed. Updated about 1 month ago * * * * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) * [Company API Reference](https://pennylane.readme.io/reference/getproducts) Ask AI --- # Understand Scopes Pennylane uses **scopes** to define what data your API token or OAuth app can access. Each scope represents a specific permission, such as _viewing invoices_ or _creating products_. Scopes ensure your integrations follow the **principle of least privilege**, giving access **only** to the data and actions they truly need. Use this page to **explore all available scopes**, understand their purpose, and determine which ones your token requires. Before You Get Started [](https://pennylane.readme.io/docs/v2-scopes#before-you-get-started) ================================================================================================ > 🔒 > > Scopes apply to both **Company** and **Firm** APIs, depending on how you authenticate: > > * **Company tokens** **\>** Access a single company’s data. > * **Firm tokens** **\>** Access multiple client companies’ data. > * **OAuth apps** **\>** Request scopes dynamically during user consent. > 💡 > > Looking for Firm-specific scopes? See the Firm API Scopes page. How Scopes Work [](https://pennylane.readme.io/docs/v2-scopes#how-scopes-work) ================================================================================== Include scopes when generating an API token or during OAuth authorization. cURL # Example (OAuth 2.0 authorization) scope=customers:all suppliers:readonly Each scope follows the same pattern: | Format | Meaning | | --- | --- | | `resource:readonly` | 🔵 Read-only access (GET endpoints only) | | `resource:all` | 🟢 Full access (read + write + delete) | > 🚧 > > If your token does not include the required scope, the API returns 403 Forbidden. Scope Domains [](https://pennylane.readme.io/docs/v2-scopes#scope-domains) ============================================================================== Scopes are organized by business domain, following the same structure as the API Reference. SALES (Customer Billing) [](https://pennylane.readme.io/docs/v2-scopes#sales-customer-billing) -------------------------------------------------------------------------------------------------- **Customer Billing** scopes give full control over sales documents, from quotes and invoices to SEPA mandates and recurring subscriptions. | Scope | Access | Description | | --- | --- | --- | | `customers:readonly` | 🔵 Read | View customers (company or individual) | | `customers:all` | 🟢 Read/Write | Create, update, and view customers — including individual and company records | | `products:readonly` | 🔵 Read | View products | | `products:all` | 🟢 Read/Write | Create, update, delete, and view products (including change events) | | `customer_invoices:readonly` | 🔵 Read | View customer invoices and matched transactions | | `customer_invoices:all` | 🟢 Read/Write | Create, update, and match customer invoices to transactions | | `quotes:readonly` | 🔵 Read | View sales quotes | | `quotes:all` | 🟢 Read/Write | Create, update, and view sales quotes | | `e_invoices:all` | 🟢 Read/Write | Import electronic invoices | | `customer_mandates:readonly` | 🔵 Read | View SEPA customer mandates | | `customer_mandates:all` | 🟢 Read/Write | Create, update, and view SEPA customer mandates | | `billing_subscriptions:readonly` | 🔵 Read | View billing subscriptions | | `billing_subscriptions:all` | 🟢 Read/Write | Create, update, and view billing subscriptions | | `commercial_documents:readonly` | 🔵 Read | View commercial documents | | `commercial_documents:all` | 🟢 Read/Write | Create, update, and view commercial documents | PURCHASES (Supplier Billing) [](https://pennylane.readme.io/docs/v2-scopes#purchases-supplier-billing) ---------------------------------------------------------------------------------------------------------- **Supplier Billing** scopes give full control over supplier documents. | Scope | Access | Description | | --- | --- | --- | | `suppliers:readonly` | 🔵 Read | View suppliers | | `suppliers:all` | 🟢 Read/Write | Create, update, and view suppliers | | `supplier_invoices:readonly` | 🔵 Read | View supplier invoices and matched transactions | | `supplier_invoices:all` | 🟢 Read/Write | Create, update, validate, import, and match supplier invoices to transactions | ACCOUNTING [](https://pennylane.readme.io/docs/v2-scopes#accounting) ------------------------------------------------------------------------ **Accounting** scopes are required for any synchronization of journal entries or exports. | Scope | Access | Description | | --- | --- | --- | | `ledger` | 🟢 Read/Write | Create, update, and view journals, ledger entries, and their attachments | | `trial_balance:readonly` | 🔵 Read | Retrieve trial balance | | `exports:fec` | 🔵 Read | Retrieve FEC (French fiscal export) | | `exports:agl` | 🔵 Read | Retrieve analytical general ledger export | | `fiscal_years:readonly` | 🔵 Read | View fiscal year information | ANALYTICS [](https://pennylane.readme.io/docs/v2-scopes#analytics) ---------------------------------------------------------------------- **Analytics scopes** control access to analytical groupings used in accounting exports. | Scope | Access | Description | | --- | --- | --- | | `categories:readonly` | 🔵 Read | View analytical categories and category groups | | `categories:all` | 🟢 Read/Write | Create, update, delete, and view analytical categories and category groups | BANKING [](https://pennylane.readme.io/docs/v2-scopes#banking) ------------------------------------------------------------------ **Banking scopes** allow you to retrieve transactions, reconcile invoices, and view categorized movements. | Scope | Access | Description | | --- | --- | --- | | `transactions:readonly` | 🔵 Read | View bank accounts | | `bank_accounts:readonly` | 🔵 Read | View bank transactions, matched invoices, and category links | CORE / SHARED [](https://pennylane.readme.io/docs/v2-scopes#core--shared) ----------------------------------------------------------------------------- **Core scopes** are shared across multiple modules - for example, attachments linked to invoices or journal entries. | Scope | Access | Description | | --- | --- | --- | | `file_attachments:readonly` | 🔵 Read | View attached files | | `file_attachments:all` | 🟢 Read/Write | Upload, update, delete, and view file attachments | Testing Your Scopes [](https://pennylane.readme.io/docs/v2-scopes#testing-your-scopes) ========================================================================================== You can verify both your token’s scopes and your user/company context with: cURL GET /me The response includes: * your company ID and user information, * and the list of active scopes for your token. > ✍️ > > This endpoint does not require a dedicated scope. All valid tokens can access it to confirm authentication and scope configuration. There are currently no public endpoints to manage users programmatically — user management is handled directly in the Pennylane interface. Tips for Developers [](https://pennylane.readme.io/docs/v2-scopes#tips-for-developers) ========================================================================================== > 💡 > > For Company tokens, select scopes in your dashboard under **Settings > Connectivity > Developers**. For Firm tokens, go to **Settings > Firm Tokens**. For **OAuth apps**, include all required scopes in your `scope` parameter. Troubleshooting [](https://pennylane.readme.io/docs/v2-scopes#troubleshooting) ================================================================================== | Error | Cause | Fix | | --- | --- | --- | | `403 Forbidden` | Missing or invalid scope | Regenerate token with required scopes | | `401 Unauthorized` | Invalid token | Check Authorization header | | `422 Unprocessable Entity` | Malformed request body | Validate JSON schema | > 📘 > > This list is current as of **October 2025**. Always refer to the [official Scopes Reference](https://pennylane.readme.io/reference/scopes) > for the most up-to-date list of available scopes. Updated 3 days ago * * * * [Create a Company API Token](https://pennylane.readme.io/docs/generating-my-api-token) * [Create a Firm API Token](https://pennylane.readme.io/docs/firm-api) * [Implement OAuth 2.0](https://pennylane.readme.io/docs/oauth-20-walkthrough) Ask AI --- # Error Handling & Status Codes When working with the Pennylane API, you may encounter validation issues, missing permissions, or temporary rate limits. This guide explains how errors are returned, what the most common status codes mean, and how to handle them efficiently in your integration. Pennylane uses **standard HTTP status codes** and a **consistent JSON error schema**, making failures predictable and easy to debug. How Errors Are Structured [](https://pennylane.readme.io/docs/error-handling-status-codes#how-errors-are-structured) ======================================================================================================================== All errors returned by the API include: * an **HTTP status code** * a **JSON error payload** * optional **details** to help identify the field or rule causing the failure Example: JSON { "error": "unprocessable_entity", "message": "Missing required field: customer_id", "details": { "field": "customer_id", "issue": "is required" } } > 💡 > > **Tip:** The `details` object is the most actionable part of the response for debugging validation errors \`(422)\`\`. > > Most issues can be resolved by inspecting it carefully. HTTP Status Code Families [](https://pennylane.readme.io/docs/error-handling-status-codes#http-status-code-families) ======================================================================================================================== Before diving into individual codes, here is how to interpret the main categories: | Family | Meaning | When it Happens | | --- | --- | --- | | **2xx** | Success | The request is valid and processed | | **4xx** | Client error | Something must be fixed in the request | | **5xx** | Server error | Temporary issue on Pennylane’s side | HTTP Status Codes Overview [](https://pennylane.readme.io/docs/error-handling-status-codes#http-status-codes-overview) ========================================================================================================================== 2xx - Success [](https://pennylane.readme.io/docs/error-handling-status-codes#2xx---success) ------------------------------------------------------------------------------------------------ | Code | Meaning | | --- | --- | | **200 OK** | The request succeeded. | | **201 Created** | A new resource was created (invoice, ledger entry, attachment). | | **204 No Content** | The request succeeded without a response body (e.g., sending an invoice by email). | 4xx - Client Errors [](https://pennylane.readme.io/docs/error-handling-status-codes#4xx---client-errors) ------------------------------------------------------------------------------------------------------------ Errors caused by problems in your request: missing token, invalid scope, malformed payload, etc. 400 - Bad Request Your request payload contains invalid or missing data. **Typical causes:** * invalid JSON format * wrong field type (e.g., number instead of string) * unsupported field * amounts not sent as strings (e.g., `"120.00"`) **How to fix:** Validate your payload before sending it. 401 - Unauthorized The access token is missing, invalid, or expired. **How to fix:** * check the `Authorization: Bearer ` header * refresh or regenerate the token * ensure you’re using the correct token type (Company / Firm / OAuth) 403 - Forbidden Your token is valid but does not include the required scope. **How to fix:** Regenerate a token that includes the missing scope (e.g., `customer_invoices:all`). 404 - Not Found The resource does not exist or cannot be accessed. **Typical causes:** * wrong or outdated ID * incorrect endpoint * accessing a record belonging to a different company 409 - Conflict Your request conflicts with existing data. **Common example:** Importing a supplier or customer invoice using a PDF already present in Pennylane. **How to fix:** Avoid reusing the same `file_attachment_id`. Only import each document once (idempotency is **not** enforced except for supplier invoice imports). 422 - Unprocessable Entity The request is syntactically correct but violates a business validation rule. **Typical causes:** * totals or VAT mismatch * unbalanced ledger entry lines * invalid VAT code * missing required business field * duplicate references in invoice creation **How to fix:** Inspect the response’s `details` field and correct the payload. 429 - Too Many Requests You exceeded the API’s rate limit. **How to fix:** * implement retry logic * respect the `Retry-After` header * reduce request bursts 5xx - Server Errors [](https://pennylane.readme.io/docs/error-handling-status-codes#5xx---server-errors) ------------------------------------------------------------------------------------------------------------ These errors are rare and typically temporary. | Code | Meaning | | --- | --- | | **500 Internal Server Error** | Unexpected server failure. | | **503 Service Unavailable** | Temporary outage or maintenance. | **How to fix:** Retry with exponential backoff. Understanding the Error Payload [](https://pennylane.readme.io/docs/error-handling-status-codes#understanding-the-error-payload) ==================================================================================================================================== Most error responses include: | Field | Description | | --- | --- | | `error` | Machine-readable code (`unprocessable_entity`, `forbidden`, etc.) | | `message` | Human-readable explanation | | `details` | Additional context (invalid field, totals mismatch, duplicate reference): | Missing scope [](https://pennylane.readme.io/docs/error-handling-status-codes#missing-scope) ------------------------------------------------------------------------------------------------ JSON { "error": "forbidden", "message": "Missing scope: customer_invoices:all" } Unbalanced ledger entry [](https://pennylane.readme.io/docs/error-handling-status-codes#unbalanced-ledger-entry) -------------------------------------------------------------------------------------------------------------------- JSON { "error": "unprocessable_entity", "message": "Entry lines are not balanced", "details": { "debit_total": "100.00", "credit_total": "80.00" } } Duplicate supplier invoice [](https://pennylane.readme.io/docs/error-handling-status-codes#duplicate-supplier-invoice) -------------------------------------------------------------------------------------------------------------------------- JSON { "status": 409, "error": "A document with ID 2058167880 already exists with such attachment." } How Pennylane Validates Requests [](https://pennylane.readme.io/docs/error-handling-status-codes#how-pennylane-validates-requests) ====================================================================================================================================== To ensure data consistency across accounting and invoicing workflows, Pennylane performs: * **schema validation** (field types, required objects, JSON structure) * **business validation** (totals, VAT rates, date rules, ledger balance) Most 422 errors originate from the second category. Handling Errors in Your Integration [](https://pennylane.readme.io/docs/error-handling-status-codes#handling-errors-in-your-integration) ============================================================================================================================================ ### **1\. Validate before submitting** [](https://pennylane.readme.io/docs/error-handling-status-codes#1-validate-before-submitting) To avoid 400/422 responses: * check required fields * validate totals and VAT consistency * ensure debit/credit lines balance * use strings for all amount fields (e.g., `"120.00"`) * verify that entity IDs exist before referencing them ### **2\. Implement safe retry logic** [](https://pennylane.readme.io/docs/error-handling-status-codes#2-implement-safe-retry-logic) Retry only when appropriate: | Should retry? | Status codes | Reason | | --- | --- | --- | | ✅ Yes | 429, 500, 503 | Temporary or rate-limited errors | | ❌ No | 400, 401, 403, 404, 422 | Requires fixing the request | ### **3\. Avoid duplicate POST requests** [](https://pennylane.readme.io/docs/error-handling-status-codes#3-avoid-duplicate-post-requests) Pennylane does **not** enforce idempotency on creation endpoints such as: * `/customer_invoices` * `/ledger_entries` If the same request is sent twice, you may create duplicates. ### **4\. Log full error bodies** [](https://pennylane.readme.io/docs/error-handling-status-codes#4-log-full-error-bodies) Always store: * the status code * `error` and `message` * `details` (whenever available) This makes debugging significantly easier. Updated 25 days ago * * * * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) * [Rate Limiting in API v2](https://pennylane.readme.io/docs/rate-limiting-1) * [Understanding VAT Rates on Ledger Accounts](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts) Ask AI --- # 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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#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](https://pennylane.readme.io/docs/create-a-ledger-entry) > Step 1 | Identifying the Journal [](https://pennylane.readme.io/docs/create-a-ledger-entry#step-1--identifying-the-journal) =============================================================================================================================== Each ledger entry must belong to a journal (sales, purchases, bank, or general). cURL curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/journals' \ -H 'Authorization: Bearer ' ✅ **Result**: cURL { "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 [](https://pennylane.readme.io/docs/create-a-ledger-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 curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"512"}]' \ -H 'Authorization: Bearer 📘 > > **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](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts) Step 3 | Creating the Ledger Entry [](https://pennylane.readme.io/docs/create-a-ledger-entry#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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#example-request) ---------------------------------------------------------------------------------------------- cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/ledger_entries \ -H "Authorization: Bearer " \ -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** cURL { "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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#step-4--optional-attach-a-supporting-document) ============================================================================================================================================================= You can attach a PDF or other proof document to a ledger entry. 1\. Upload the File [](https://pennylane.readme.io/docs/create-a-ledger-entry#1-upload-the-file) ---------------------------------------------------------------------------------------------------- cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/file_attachments \ -H "Authorization: Bearer " \ -H "Content-Type: multipart/form-data" \ -F [email protected] ✅ **Result**: { "id": 34567, "filename": "payment-proof.pdf", "status": "uploaded" } 2\. Linking It to Your Entry [](https://pennylane.readme.io/docs/create-a-ledger-entry#2-linking-it-to-your-entry) ---------------------------------------------------------------------------------------------------------------------- curl --request POST \ --url https://app.pennylane.com/api/external/v2/ledger_entries \ -H "Authorization: Bearer " \ -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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#step-5--handling-multi-currency-entries) =============================================================================================================================================== You can create entries in a different currency using the currency field. cURL { "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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#step-6--tracking-changes) ================================================================================================================= To monitor updates or synchronize accounting data, use the changelog endpoint: cURL 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 " ✅ **Result:** Returns entries created, updated, or deleted since the specified date. Common Pitfalls [](https://pennylane.readme.io/docs/create-a-ledger-entry#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 [](https://pennylane.readme.io/docs/create-a-ledger-entry#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 about 2 months ago * * * * [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/supplier-invoicing) * [Create a Customer Invoice via API](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case) Ask AI --- # 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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#before-you-get-started) ===================================================================================================================== **Required scopes (Company API v2)** | **Scope** | **Features** | | --- | --- | | `ledger` | create entries, list journals & ledger accounts | | `file_attachments:all` | optional – 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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#step-1--optional-verify-authentication) ======================================================================================================================================================== Before posting sales entries, confirm that your token and environment are correctly set up. cURL curl https://app.pennylane.com/api/external/v2/me \ -H "Authorization: Bearer " ✅ 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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#step-2--identify-or-creating-the-pos-journal) ==================================================================================================================================================================== List Journals (Let the User Choose) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#list-journals-let-the-user-choose) --------------------------------------------------------------------------------------------------------------------------------------------- cURL curl --request GET \ --url https://app.pennylane.com/api/external/v2/journals \ -H "Authorization: Bearer " **Typical codes** | **Code** | **Journal** | | --- | --- | | `VT` | Sales journal | | `HA` | Purchases journal | | `BQ` | Bank journal | | `OD` | General journal | > ✅ > > **Result** > > Store the chosen **`journal_id`** and reuse it for all POS entries. Create a Dedicated POS Journal (if Missing) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#create-a-dedicated-pos-journal-if-missing) ------------------------------------------------------------------------------------------------------------------------------------------------------------- cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/journals \ -H "Authorization: Bearer " \ -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) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#revenue-net-sales) ------------------------------------------------------------------------------------------------------------- Revenue accounts typically start with **706** (services) or **707** (goods). cURL curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"706"}]' \ -H "Authorization: Bearer " > 💡 > > **Tip**: Keep one revenue account per VAT rate (e.g., 7061 = 10%, 7062 = 20%) for easier reporting. If missing, create one: cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/ledger_accounts \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "number": "7062", "label": "Sales – VAT 20%" VAT (Output VAT) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts) > page. VAT accounts often start with 4457… cURL curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"4457"}]' \ -H "Authorization: Bearer " > 📘 > > **Note**: These accounts usually exist already - avoid duplicates. Payment Types [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#payment-types) --------------------------------------------------------------------------------------------------- Payment method accounts often use **511xxx** (payment intermediaries) or **530** (cash). cURL # 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 " # 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 " If missing, create one: cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/ledger_accounts \ -H "Authorization: Bearer " \ -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) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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 curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"658"}]' \ -H "Authorization: Bearer " Step 5 | (Optional) Attach the Ticket Z PDF File [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#step-5--optional-attach-the-ticket-z-pdf-file) ====================================================================================================================================================================== cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/file_attachments \ -H "Authorization: Bearer " \ -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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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 " \ -H "Content-Type: application/json" \ -d '{ "date": "2025-01-01", "label": "Ticket Z – Main Register – 2025-01-01", "journal_id": , "ledger_entry_lines": [\ { "debit": "0.00", "credit": "90.91", "ledger_account_id": },\ { "debit": "0.00", "credit": "9.09", "ledger_account_id": },\ { "debit": "100.00","credit": "0.00", "ledger_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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#step-7--validate--troubleshoot) ======================================================================================================================================= **Common errors** [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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) [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#verifying-your-entries) --------------------------------------------------------------------------------------------------------------------- cURL 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":}]' \ -H "Authorization: Bearer " > 📘 > > **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 [](https://pennylane.readme.io/docs/how-to-build-a-pos-integration#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. Updated about 1 month ago * * * * [Implement OAuth 2.0](https://pennylane.readme.io/docs/oauth-20-walkthrough) * [Create Ledger Entries via API](https://pennylane.readme.io/docs/create-a-ledger-entry) * [Journals](https://pennylane.readme.io/reference/journals) * [Ledger Accounts](https://pennylane.readme.io/reference/ledger-accounts) * [Ledger Entries](https://pennylane.readme.io/reference/ledger-entries) Ask AI --- # Filter API Data How does it work ? [](https://pennylane.readme.io/docs/setting-up-filters#how-does-it-work-) ------------------------------------------------------------------------------------------------ You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. A filter object is always a combination of the three following keys : | Name | Description | | --- | --- | | field | The field of the concerned resource you want to filter by | | operator | The operator you want to compare by | | value | The value of the given field you want to filter by | Available operators [](https://pennylane.readme.io/docs/setting-up-filters#available-operators) --------------------------------------------------------------------------------------------------- | Operator | Description | | --- | --- | | `eq` | equal to | | `not_eq` | not equal to | | `lt` | lesser than | | `lteq` | lesser than or equal to | | `gt` | greater than | | `gteq` | greater than or equal to | | `in` | field is included in value (must be an array) | | `not_in` | field is not included in value (must be an array) | | `start_with` | search text (ILIKE '123%') | Example #1 : Filter the invoices per date [](https://pennylane.readme.io/docs/setting-up-filters#example-1--filter-the-invoices-per-date) --------------------------------------------------------------------------------------------------------------------------------------------- Let's say you want to retrieve all invoices from the 2024-01-01. You need the field **date**, with operator **gteq**, with value set to **2024-01-01**. Ruby filters = [\ { \ field: 'date', \ operator: 'gteq', \ value: '2024-01-01'\ },\ ].to_json Example #2 : Filter the invoices per multiple customer id [](https://pennylane.readme.io/docs/setting-up-filters#example-2--filter-the-invoices-per-multiple-customer-id) ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Now, let's say you want to retrieve all invoices from 2025 of two customers with id is ONE\_CUSTOMER\_ID and TWO\_CUSTOMER\_ID. You need 2 filters. When filtering by `customer_id`, the `in` operator will return the invoices with `customer_id` similar to the passed values. See the following example with two filters : Ruby filters = [\ { \ field: 'date', \ operator: 'gteq', \ value: '2025-01-01',\ },\ { \ field: 'customer_id', \ operator: 'in', \ value: [ONE_CUSTOMER_ID, TWO_CUSTOMER_ID],\ },\ ].to_json Updated 3 months ago * * * Ask AI --- # Import a Supplier Invoice via API Use this guide to import supplier (purchase) invoices into Pennylane using the API — for example, from your ERP, OCR, or document capture tool. Goal [](https://pennylane.readme.io/docs/supplier-invoicing#goal) ===================================================================== This tutorial explains how to automatically import supplier invoices so they appear in your workspace, linked to the right supplier and ready for accounting and reconciliation. You will learn how to: 1. Upload a supplier invoice file (PDF) 2. Import the invoice via the API 3. (Optional) Categorize it for reporting > ✅ > > **End Result** > > Your supplier invoice appears in Pennylane, linked to the right supplier and ledger accounts, fully integrated into your accounting workflow. > 👥 > > **Who is this for?** > > Developers and partners building integrations that sync supplier invoices from external systems into Pennylane. > 🔒 > > **Authentication** > > Partner integrations must use **OAuth 2.0**. > > In sandbox, you can test with a **Company API token**, but OAuth 2.0 is required for Marketplace production integrations. Before You Get Started [](https://pennylane.readme.io/docs/supplier-invoicing#before-you-get-started) ========================================================================================================= **Required scopes (Company API v2)** | **Scope** | **Features** | | --- | --- | | `supplier_invoices:all` | Create or import supplier invoices | | `file_attachments:all` | Upload and attach supplier invoice PDFs | | _(optional)_ `ledger_accounts:readonly` | Retrieve account IDs for accurate accounting mapping | | _(optional)_ `categories:all` | Categorize 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 **`supplier_id`** (existing or newly created) * A valid **invoice file (PDF)** to attach * _(Optional)_ **ledger account IDs** — e.g., **60x** for purchases/expenses, **4456** for deductible VAT, **401** for suppliers > ℹ️ > > **IDs are company-specific** > > Supplier and ledger account IDs differ per company. Store them per tenant to avoid mismatches. > 📘 > > **See also:** > > * Authentication Overview > * [Understand V2 Scopes](https://pennylane.readme.io/docs/v2-scopes) > Step 1 | (Optional) Verifying Authentication [](https://pennylane.readme.io/docs/supplier-invoicing#step-1--optional-verifying-authentication) ================================================================================================================================================== Before importing invoices, confirm that your token and environment are correctly configured. cURL curl https://app.pennylane.com/api/external/v2/me \ -H "Authorization: Bearer " ✅ **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**. Step 2 | Uploading the Invoice File [](https://pennylane.readme.io/docs/supplier-invoicing#step-2--uploading-the-invoice-file) ================================================================================================================================== Before importing the supplier invoice, upload the PDF file to Pennylane. cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/file_attachments \ -H "Authorization: Bearer " \ -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. **Example response:** cURL { "id": 13245, "filename": "invoice-october.pdf", "status": "uploaded" } > 💡 > > **Accepted file format** > > Only **PDF** files are supported for supplier invoice imports. > > The `/file_attachments` endpoint accepts files up to **100 MB**. > > Uploading another format for an invoice will return a validation error (typically `400 Bad Request`). Step 3 | Importing the Supplier Invoice [](https://pennylane.readme.io/docs/supplier-invoicing#step-3--importing-the-supplier-invoice) ========================================================================================================================================== Now that your invoice file is uploaded, use the import endpoint to create the corresponding invoice in Pennylane. cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/supplier_invoices/import \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_attachment_id": 13245, "supplier_id": 98123, "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": 601002,\ "currency_amount": "120.00",\ "currency_tax": "20.00",\ "vat_rate": "FR_200"\ }\ ] }' ✅ **Result** The invoice is created and appears in your Pennylane workspace, linked to the specified supplier. > 💡 > > **Tip: Amount field types** > > All numeric amounts (e.g. `currency_amount`, `currency_tax`, `currency_amount_before_tax`) must be provided as **strings**, not numbers. **Example response:** cURL { "id": 4431, "status": "imported", "supplier_name": "ACME Office Supplies", "currency_amount": "120.00" } > ⚠️ > > **Important** > > The sum of all `invoice_lines.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 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. > > | **Code** | **Description** | **Rate** | > | --- | --- | --- | > | `FR_200` | Standard VAT France | 20% | > | `FR_100` | Reduced VAT France | 10% | > | `FR_055` | Reduced VAT France | 5.5% | > | `exempt` | Exempt (0%) | 0% | > | `any` | No specific VAT code | — | > > For the full list of supported VAT codes, see the API Reference for [Import Supplier Invoice](https://pennylane.readme.io/reference/importsupplierinvoice) > . Step 4 | Validating the Import [](https://pennylane.readme.io/docs/supplier-invoicing#step-4--validating-the-import) ======================================================================================================================== You can verify that the invoice was successfully created: cURL curl --request GET \ --url https://app.pennylane.com/api/external/v2/supplier_invoices/4431 \ -H "Authorization: Bearer " ✅ **Result** cURL { "id": 4431, "status": "imported", "currency_amount": "120.00", "supplier_name": "ACME Office Supplies" } > ⚠️ > > **Common errors** > > * `401 Unauthorized` → Invalid or expired token > * `403 Forbidden` → Missing scope (`supplier_invoices:all`) > * `422 Unprocessable Entity` → VAT or totals mismatch > 💡 > > **Tip:** In sandbox, you can safely test imports with fake suppliers and sample amounts. Step 5 | (Optional) Categorizing the Invoice [](https://pennylane.readme.io/docs/supplier-invoicing#step-5--optional-categorizing-the-invoice) ================================================================================================================================================== Categorization helps analyze expenses by department, cost center, or activity. 1\. Get or Create a Category [](https://pennylane.readme.io/docs/supplier-invoicing#1-get-or-create-a-category) ------------------------------------------------------------------------------------------------------------------- # List categories GET /api/external/v2/categories # Create a new category POST /api/external/v2/categories 2\. Assign the Category to the Invoice [](https://pennylane.readme.io/docs/supplier-invoicing#2-assign-the-category-to-the-invoice) --------------------------------------------------------------------------------------------------------------------------------------- cURL curl --request PUT \ --url https://app.pennylane.com/api/external/v2/supplier_invoices/{invoice_id}/categories \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[\ { "id": 123, "weight": 1.0 }\ ]' > 📘 > > **Note** > > `weight` represents the allocation percentage (`1.0` = 100%, `0.5` = 50%). Multiple categories are supported. Test in Sandbox or Postman [](https://pennylane.readme.io/docs/supplier-invoicing#test-in-sandbox-or-postman) ================================================================================================================= You can test this workflow safely using your Pennylane Sandbox or the official Postman collection. * Collection: `Pennylane API v2 → Supplier Invoices → Import` * Replace `` with your sandbox token * Use real `supplier_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 [](https://pennylane.readme.io/docs/supplier-invoicing#common-pitfalls) =========================================================================================== | Issue | Likely Cause | How to Fix | | --- | --- | --- | | `400 Bad Request` | Invalid payload: unexpected field or missing required property | Check field names, required fields, and data types | | `401 Unauthorized` | Missing or expired token | Refresh token or fix header | | `403 Forbidden` | Missing scope | Add `supplier_invoices:all` | | `404 File not found` | Invalid/expired `file_attachment_id` | Re-upload via `/file_attachments` | | `422 Entry lines are not balanced` | HT/TVA/TTC mismatch | Recalculate totals | Best Practices [](https://pennylane.readme.io/docs/supplier-invoicing#best-practices) ========================================================================================= * **Validate before import** — totals & VAT consistency * **Use appropriate accounts** — e.g., `60x` expenses, `4456` deductible VAT, `401` suppliers * **Secure tokens** — rotate & store safely * **Test in sandbox first** — then roll out to production > 💡 > > **Duplicate handling** > > Pennylane automatically prevents duplicates by rejecting supplier invoices whose attached PDF (`file_attachment_id`) already exists in the workspace — whether it was imported manually or via API. > > If you try to re-import the same file, the API returns `422 Unprocessable Entity`. How Pennylane Handles Accounting [](https://pennylane.readme.io/docs/supplier-invoicing#how-pennylane-handles-accounting) ============================================================================================================================= When you import a supplier invoice: * Pennylane automatically creates **expense entries** in your accounting ledgers. * The entries appear in your **Purchases journal**. * The invoice is visible in **Invoicing → Supplier Invoices** in your workspace. > 📘 > > **Note** > > Entries are generated according to your company’s chart of accounts and the `ledger_account_id` values provided in your payload. Updated about 2 months ago * * * * [Import a Customer Invoice via API](https://pennylane.readme.io/docs/customer-invoicing) * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) Ask AI --- # 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 [](https://pennylane.readme.io/docs/customer-invoicing#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 [](https://pennylane.readme.io/docs/customer-invoicing#before-you-get-started) ========================================================================================================= **Required scopes (Company API v2)** | **Scope** | **Features** | | --- | --- | | `customer_invoices:all` | Create or import customer invoices | | `file_attachments:all` | Upload and attach invoice PDFs | | _(optional)_ `products:readonly` | Retrieve products for invoice lines | | _(optional)_ `ledger_accounts:readonly` | Retrieve account IDs for proper accounting mapping | | _(optional)_ `categories:all` | Categorize 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**: > > * Authentication Overview > * [Understanding Scopes](https://pennylane.readme.io/docs/v2-scopes) > Step 1 | (Optional) Verifying Authentication [](https://pennylane.readme.io/docs/customer-invoicing#step-1--optional-verifying-authentication) ================================================================================================================================================== Before importing invoices, confirm that your token and environment are correctly set up. cURL curl https://app.pennylane.com/api/external/v2/me \ -H "Authorization: Bearer " ✅ **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 [](https://pennylane.readme.io/docs/customer-invoicing#step-2--uploading-the-invoice-file) ================================================================================================================================== Before importing your invoice, upload the PDF file to Pennylane. cURL curl --request POST \ --url https://app.pennylane.com/api/external/v2/file_attachments \ -H "Authorization: Bearer " \ -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** cURL { "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 [](https://pennylane.readme.io/docs/customer-invoicing#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 curl --request POST \ --url https://app.pennylane.com/api/external/v2/customer_invoices/import \ -H "Authorization: Bearer " \ -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** cURL { "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. > > | **Code** | **Description** | **Rate** | > | --- | --- | --- | > | `FR_200` | Standard VAT France | 20% | > | `FR_100` | Reduced VAT France | 10% | > | `FR_055` | Reduced VAT France | 5.5% | > | `exempt` | Exempt (0%) | 0% | > | `any` | No specific VAT code | — | > > For the full list of supported VAT codes, see the API Reference for the [Create a customer invoice](https://chatgpt.com/g/g-p-68c3e456799c81918948eb2b87ead792-pennylane-api/c/68f9e631-fa48-832e-a67d-63448f30c9cb) > endpoint. Step 4 | Validating the Import [](https://pennylane.readme.io/docs/customer-invoicing#step-4--validating-the-import) ======================================================================================================================== After importing the invoice, verify that it was successfully created in Pennylane. cURL curl --request GET \ --url https://app.pennylane.com/api/external/v2/customer_invoices/5678 \ -H "Authorization: Bearer " ✅ **Result** A successful response confirms the invoice exists and was imported correctly. **Example response** cURL { "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 [](https://pennylane.readme.io/docs/customer-invoicing#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 [](https://pennylane.readme.io/docs/customer-invoicing#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 [](https://pennylane.readme.io/docs/customer-invoicing#2-assign-the-category-to-the-invoice) --------------------------------------------------------------------------------------------------------------------------------------- cURL curl --request PUT \ --url https://app.pennylane.com/api/external/v2/customer_invoices/{invoice_id}/categories \ -H "Authorization: Bearer " \ -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` [](https://pennylane.readme.io/docs/customer-invoicing#when-to-use-import-vs-create) =========================================================================================================================== Both endpoints create customer invoices, but they serve different workflows: | **Action** | **Endpoint** | **When to use** | | --- | --- | --- | | Uploading and attaching a PDF file | `/customer_invoices/import` | When your invoice is already generated by another system (e.g., POS, ERP, or billing software) | | Generating invoices directly | `/customer_invoices` | When 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 [](https://pennylane.readme.io/docs/customer-invoicing#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 `` 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 [](https://pennylane.readme.io/docs/customer-invoicing#common-pitfalls) =========================================================================================== | Issue | Likely Cause | How to Fix | | --- | --- | --- | | `400 Bad Request` | Invalid payload — unexpected field or missing required property | Check field names, required fields, and data types | | `401 Unauthorized` | Missing or expired OAuth token | Refresh token or check header format | | `403 Forbidden` | Missing scope (`customer_invoices:all`) | Add missing scope to your OAuth app | | `404 File not found` | Uploaded file expired or deleted | Re-upload using `/file_attachments` | | `422 Entry lines are not balanced` | Totals mismatch between HT / TVA / TTC | Recalculate totals | | Duplicate invoices | Re-importing the same payload | Implement 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 [](https://pennylane.readme.io/docs/customer-invoicing#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 [](https://pennylane.readme.io/docs/customer-invoicing#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. Updated about 1 month ago * * * * [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/supplier-invoicing) * [Create a Customer Invoice via API](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case) * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) Ask AI --- # Filter Ledger Entries You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for ledger entries [](https://pennylane.readme.io/docs/filter-my-ledger-entries#allowed-filter-combinations-for-ledger-entries) --------------------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | journal\_id | The ledger entry journal id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | date | The ledger entry date | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | created\_at | The ledger entry created\_at | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | updated\_at | The ledger entry updated\_at | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | Updated 3 months ago * * * Ask AI --- # Filter Supplier Invoices Use filters to retrieve only the supplier invoices that match your criteria (supplier, date range, invoice number, category, external reference, etc.). Filtering helps reduce dataset size, improve performance, and focus your integration on relevant invoices. This guide shows how to use the `filters[]` parameter on the **/supplier\_invoices** endpoint and lists all supported fields and operators. > 📘 > > **Reminder:** Filters use a JSON array passed as the filters query parameter. See Filter API Data for the general filtering syntax. How Filtering Works [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#how-filtering-works) ============================================================================================================ Filtering supplier invoices uses the API’s standard `filters[]` format. Example filter array: JSON [\ { "field": "supplier_id", "operator": "eq", "value": "sup_123" },\ { "field": "date", "operator": "gteq", "value": "2024-01-01" }\ ] Example request: cURL GET /supplier_invoices?filters=[{"field":"supplier_id","operator":"eq","value":"sup_123"}] You can combine as many filters as needed. Common Use Cases [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#common-use-cases) ====================================================================================================== Developers typically use filters to: * Retrieve invoices for a specific supplier * Pull invoices issued within a given period * Match supplier invoices by invoice number or external reference * Filter by category (purchases, expenses, subcontracting, etc.) * Build BI dashboards or automated reconciliation workflows Examples [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#examples) ====================================================================================== Filter by supplier ID [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#filter-by-supplier-id) ---------------------------------------------------------------------------------------------------------------- cURL GET /supplier_invoices?filters=[\ {"field":"supplier_id","operator":"eq","value":"sup_987"}\ ] Filter by date range [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#filter-by-date-range) -------------------------------------------------------------------------------------------------------------- cURL GET /supplier_invoices?filters=[\ {"field":"date","operator":"gteq","value":"2024-01-01"},\ {"field":"date","operator":"lteq","value":"2024-03-31"}\ ] Filter by invoice number [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#filter-by-invoice-number) ---------------------------------------------------------------------------------------------------------------------- cURL GET /supplier_invoices?filters=[\ {"field":"invoice_number","operator":"eq","value":"F-2024-0091"}\ ] Filter by external reference [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#filter-by-external-reference) ------------------------------------------------------------------------------------------------------------------------------ GET /supplier_invoices?filters=[\ {"field":"external_reference","operator":"eq","value":"ERP-INV-4452"}\ ] Available Fields and Operators [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#available-fields-and-operators) ================================================================================================================================== | **Field** | **Description** | **Value Type** | **Operators** | | --- | --- | --- | --- | | `id` | Supplier invoice ID | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `supplier_id` | Supplier ID | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `invoice_number` | Supplier invoice number | String | `eq`, `not_eq`, `in`, `not_in` | | `date` | Invoice issue date (ISO 8601) | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `category_id` | Assigned purchase category | String | `in` | | `external_reference` | External reference from another system | String | `eq`, `not_eq`, `in`, `not_in` | > 💡 > > **Tip:** Use ISO 8601 format (YYYY-MM-DD) for date filters. Tips & Troubleshooting [](https://pennylane.readme.io/docs/filter-my-supplier-invoices#tips--troubleshooting) ================================================================================================================= * **URL-encode the filter array** to avoid 400 errors. * Combine filters with pagination when retrieving large datasets (using the pagination mechanism supported by the endpoint). * Use `in` / `not_in` for multiple supplier IDs or invoice numbers. * Filtering by `invoice_number` is **exact-match**, not a text search. * To retrieve recently updated invoices, filter by `updated_at` (if available), or use the **Changelog API**. Updated 25 days ago * * * * [Filter API Data](https://pennylane.readme.io/docs/setting-up-filters) * [Filter Customer Invoices](https://pennylane.readme.io/docs/filter-my-customer-invoices) * [List suppliers](https://pennylane.readme.io/reference/getsuppliers) * [Track Data Changes with the API](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) Ask AI --- # 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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#before-you-get-started) ========================================================================================================================= **Required scopes (Company API v2)** | **Scope** | **Features** | | --- | --- | | `customer_invoices:all` | Create and send customer invoices | | `(optional)*` transactions:readonly\` | Retrieve payment or reconciliation data | | _(optional)_ `products:readonly` | Retrieve product details | | _(optional)_ `ledger_accounts:readonly` | Fetch accounting mappings | | _(optional)_ `categories:all` | Categorize invoices | | _(optional)_ `file_attachments:all` | Attach 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:** > > * Authentication Overview > * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) > Step 1 | (Optional) Verifying Authentication [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#step-1--optional-verifying-authentication) ================================================================================================================================================================== Before creating invoices, confirm that your token and environment are correctly set up. cURL curl https://app.pennylane.com/api/external/v2/me \ -H "Authorization: Bearer " ✅ 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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#find-or-create-a-customer) ------------------------------------------------------------------------------------------------------------------------------- Check if the customer exists: cURL curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/customers?filter=[{"field":"external_reference","operator":"eq","value":"ACME_001"}]' \ -H 'Authorization: Bearer ' If not, create a company customer: cURL curl --request POST \ --url 'https://app.pennylane.com/api/external/v2/company_customers' \ -H 'Authorization: Bearer ' \ -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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#optional-retrieve-products) ----------------------------------------------------------------------------------------------------------------------------------- If your invoice references products: cURL curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/products' \ -H 'Authorization: Bearer ' > 💡 > > **Tip:** Using products ensures consistent pricing and VAT handling across invoices. (Optional) Retrieve Ledger Accounts [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#optional-retrieve-ledger-accounts) ------------------------------------------------------------------------------------------------------------------------------------------------- For precise accounting, retrieve available accounts: cURL curl --request GET \ --url 'https://app.pennylane.com/api/external/v2/ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"706"}]' \ -H 'Authorization: Bearer ' > 📘 > > **Common ledger accounts** > > * `706xxx` → Service revenue > * `707xxx` → Product sales Step 3 | Creating the Customer Invoice [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#step-3--creating-the-customer-invoice) ======================================================================================================================================================== Once references are ready, create the invoice directly in Pennylane. Minimal Example [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#minimal-example) ----------------------------------------------------------------------------------------------------------- cURL curl --request POST \ --url "https://app.pennylane.com/api/external/v2/customer_invoices" \ -H "Authorization: Bearer " \ -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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#invoice-with-product-and-template) ----------------------------------------------------------------------------------------------------------------------------------------------- cURL curl --request POST \ --url "https://app.pennylane.com/api/external/v2/customer_invoices" \ -H "Authorization: Bearer " \ -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** cURL { "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. > > | **Code** | **Description** | **Rate** | > | --- | --- | --- | > | `FR_200` | Standard VAT France | 20% | > | `FR_100` | Reduced VAT France | 10% | > | `FR_055` | Reduced VAT France | 5.5% | > | `exempt` | Exempt (0%) | 0% | > | `any` | No specific VAT code | — | > > For the full list of supported VAT codes, see the API Reference for [Create a Customer Invoice](https://pennylane.readme.io/reference/postcustomerinvoices#/) > . Step 4 | Verifying the Invoice [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#step-4--verifying-the-invoice) ======================================================================================================================================== Check the created invoice: cURL curl --request GET \ --url "https://app.pennylane.com/api/external/v2/customer_invoices/9876" \ -H "Authorization: Bearer " ✅ **Result** cURL { "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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#step-5--optional-actions) ============================================================================================================================== 1\. Send the Invoice by Email [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#1-send-the-invoice-by-email) ------------------------------------------------------------------------------------------------------------------------------------- cURL curl --request POST \ --url "https://app.pennylane.com/api/external/v2/customer_invoices/9876/send_by_email" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "recipients": ["[email protected]"] }' 2\. Categorize the Invoice [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#2-categorize-the-invoice) ------------------------------------------------------------------------------------------------------------------------------- curl --request PUT \ --url "https://app.pennylane.com/api/external/v2/customer_invoices/9876/categories" \ -H "Authorization: Bearer " \ -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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#3-track-reconciliation) --------------------------------------------------------------------------------------------------------------------------- cURL curl --request GET \ --url "https://app.pennylane.com/api/external/v2/customer_invoices/9876/matched_transactions" \ -H "Authorization: Bearer " Test in Sandbox or Postman [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#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 `` 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 [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#common-pitfalls) =========================================================================================================== | **Error** | **Likely Cause** | **How to Fix** | | --- | --- | --- | | `400 Bad Request` | Invalid payload or missing required property | Check field names and formats | | `401 Unauthorized` | Missing or expired token | Refresh or verify header | | `403 Forbidden` | Missing scope | Add `customer_invoices:all` | | `404 Not Found` | Invalid `customer_id` or endpoint | Check IDs and URL | | `422 Unprocessable Entity` | Totals or VAT mismatch | Recalculate amounts | Best Practices [](https://pennylane.readme.io/docs/create-a-customer-invoice-use-case#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 Updated about 2 months ago * * * * [Import a Customer Invoice via API](https://pennylane.readme.io/docs/customer-invoicing) * [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/supplier-invoicing) * [Understand Scopes](https://pennylane.readme.io/docs/v2-scopes) Ask AI --- # Filter Ledger Accounts You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for ledger accounts [](https://pennylane.readme.io/docs/filter-my-ledger-accounts#allowed-filter-combinations-for-ledger-accounts) ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | id | The ledger account id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | number | The ledger account number | String | `start_with` | Updated 3 months ago * * * Ask AI --- # Filter Suppliers You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for suppliers [](https://pennylane.readme.io/docs/filter-my-suppliers#allowed-filter-combinations-for-suppliers) ------------------------------------------------------------------------------------------------------------------------------------------------ | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | id | The thirdparty supplier id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | ledger\_account\_id | The thirdparty ledger account id | String | `eq`, `not_eq` | | name | The thirdparty supplier name | | `start_with` | | external\_reference | The thirdparty supplier external reference | String | `eq`, `not_eq`, `in`, `not_in` | Updated 3 months ago * * * Ask AI --- # Filter Draft Invoices Draft invoices are customer invoices that have been created but not yet validated in Pennylane. They are commonly used during invoice preparation and review workflows before final posting. Filtering draft invoices helps you retrieve the right drafts efficiently — for example to display pending drafts, sync them with an external billing system, or locate a specific draft by reference. This guide shows how to filter draft invoices using the `filters[]` parameter on the **`GET /customer_invoices`** endpoint. > 📘 > > **Reminder:** Filters use a JSON array passed as the filters query parameter. See [Filter API Data](https://pennylane.readme.io/update/docs/setting-up-filters) > for the general filtering syntax and operators. How Filtering Works [](https://pennylane.readme.io/docs/filter-my-draft-invoices#how-filtering-works) ========================================================================================================= Draft invoices are retrieved by filtering customer invoices on their status: JSON [\ { "field": "status", "operator": "eq", "value": "draft" }\ ]] Example request: cURL GET /customer_invoices?filters=[{"field":"status","operator":"eq","value":"draft"}] You can combine multiple filters to narrow down results further. Common Use Cases [](https://pennylane.readme.io/docs/filter-my-draft-invoices#common-use-cases) =================================================================================================== Filtering draft invoices is typically used to: * List invoices pending validation * Retrieve a specific draft invoice by reference * Sync draft invoices between Pennylane and an external system * Support internal review or approval workflows Examples [](https://pennylane.readme.io/docs/filter-my-draft-invoices#examples) =================================================================================== Retrieve draft invoices [](https://pennylane.readme.io/docs/filter-my-draft-invoices#retrieve-draft-invoices) ----------------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"status","operator":"eq","value":"draft"}\ ] Retrieve a specific draft invoice by invoice number [](https://pennylane.readme.io/docs/filter-my-draft-invoices#retrieve-a-specific-draft-invoice-by-invoice-number) ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"status","operator":"eq","value":"draft"},\ {"field":"invoice_number","operator":"eq","value":"TEST"}\ ] Available Fields and Operators [](https://pennylane.readme.io/docs/filter-my-draft-invoices#available-fields-and-operators) =============================================================================================================================== | **Field** | **Description** | **Value Type** | **Operators** | | --- | --- | --- | --- | | `status` | Invoice status | String | `eq`, `not_eq`, `in`, `not_in` | | `invoice_number` | Invoice reference | String | `eq`, `not_eq`, `in`, `not_in` | > ✍️ > > **Note:** Draft invoices are identified using status = "draft" on the customer invoices endpoint. Tips & Troubleshooting [](https://pennylane.readme.io/docs/filter-my-draft-invoices#tips--troubleshooting) ============================================================================================================== * Filtering by `invoice_number` uses **exact matching** (not partial search). * Always **URL-encode** the `filters[]` parameter to avoid `400 Bad Request` errors. * This endpoint returns **cursor-based pagination** (`has_more`, `next_cursor`). Use the cursor parameters supported by the API when iterating over large datasets. * If you want to retrieve validated invoices, adjust the `status` filter accordingly or remove it. Updated 25 days ago * * * * [Filter API Data](https://pennylane.readme.io/docs/setting-up-filters) * [Filter Customer Invoices](https://pennylane.readme.io/docs/filter-my-customer-invoices) * [List customer invoices](https://pennylane.readme.io/reference/getcustomerinvoices) * [Track Data Changes with the API](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) Ask AI --- # Filter Products You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for products [](https://pennylane.readme.io/docs/filter-my-products#allowed-filter-combinations-for-products) --------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | id | The product id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | Updated 3 months ago * * * Ask AI --- # Filter Credit Notes Credit notes are used to record **refunds**, **cancellations**, or **invoice adjustments**. In Pennylane’s API, credit notes are exposed through the **Customer Invoices** endpoint and can be retrieved by filtering on the invoice type. Filtering helps reduce dataset size, improve performance, and synchronize only the documents your integration needs. This guide explains how to retrieve credit notes using the `filters[]` parameter on **`GET /customer_invoices`**. > 📘 > > **Reminder:** Filters use a JSON array passed as the filters query parameter. See Filter API Data for the general filtering syntax. **How Filtering Works** [](https://pennylane.readme.io/docs/filter-my-credit-notes#how-filtering-works) ----------------------------------------------------------------------------------------------------------- Credit notes can be retrieved by filtering customer invoices on their type. Example request: cURL GET /customer_invoices?filters=[{"field":"document_type","operator":"eq","value":"credit_note"}] You can combine filters to narrow down results further (e.g., type + invoice number). Common Use Cases [](https://pennylane.readme.io/docs/filter-my-credit-notes#common-use-cases) ================================================================================================= Developers typically filter credit notes to: * Retrieve refunds issued for one or more customer invoices * Identify cancelled or adjusted invoices * Synchronize credit notes with an external accounting or ERP system * Build reconciliation or reporting workflows * Track customer refunds in BI or financial dashboards Examples [](https://pennylane.readme.io/docs/filter-my-credit-notes#examples) ================================================================================= Retrieve credit notes [](https://pennylane.readme.io/docs/filter-my-credit-notes#retrieve-credit-notes) ----------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"document_type","operator":"eq","value":"credit_note"}\ ] Retrieve a specific credit note by number [](https://pennylane.readme.io/docs/filter-my-credit-notes#retrieve-a-specific-credit-note-by-number) --------------------------------------------------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"document_type","operator":"eq","value":"credit_note"},\ {"field":"invoice_number","operator":"eq","value":"CN-2024-0012"}\ ] Retrieve multiple credit notes [](https://pennylane.readme.io/docs/filter-my-credit-notes#retrieve-multiple-credit-notes) ----------------------------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"document_type","operator":"eq","value":"credit_note"},\ {"field":"invoice_number","operator":"in","value":["CN-2024-0012","CN-2024-0015"]}\ ] Available Fields and Operators [](https://pennylane.readme.io/docs/filter-my-credit-notes#available-fields-and-operators) ============================================================================================================================= | **Field** | **Description** | **Value Type** | **Operators** | | --- | --- | --- | --- | | `document_type` | Document type (use `credit_note` for credit notes) | String | `eq`, `not_eq`, `in`, `not_in` | | `invoice_number` | Credit note reference | String | `eq`, `not_eq`, `in`, `not_in` | Tips & Troubleshooting [](https://pennylane.readme.io/docs/filter-my-credit-notes#tips--troubleshooting) ============================================================================================================ * **URL-encode the filter array** to avoid 400 errors. * Combine filters with pagination when retrieving large datasets (using the pagination mechanism supported by the endpoint). * `invoice_number` filtering is **exact match** (not a text search). * If your integration needs incremental updates, use the **Changelog API** instead of refetching all records. Updated 24 days ago * * * * [Filter API Data](https://pennylane.readme.io/docs/setting-up-filters) * [Filter Customer Invoices](https://pennylane.readme.io/docs/filter-my-customer-invoices) * [Link a credit note to a customer invoice](https://pennylane.readme.io/reference/linkcreditnote) * [Track Data Changes with the API](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) Ask AI --- # Filter Categories You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for categories [](https://pennylane.readme.io/docs/filter-my-categories#allowed-filter-combinations-for-categories) --------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | category\_group\_id | The category group id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | analytical\_code | The category analytical code | String | `eq`, `not_eq`, `in`, `not_in` | Updated 3 months ago * * * Ask AI --- # Filter Ledger Entry Lines You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for ledger entry lines [](https://pennylane.readme.io/docs/filter-my-ledger-entry-lines#allowed-filter-combinations-for-ledger-entry-lines) --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | id | The ledger entry line id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | ledger\_account\_id | The ledger account id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | date | The ledger entry line date | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | journal\_id | The ledger entry line journal id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | Allowed filter combinations for ledger entry / ledger entry lines [](https://pennylane.readme.io/docs/filter-my-ledger-entry-lines#allowed-filter-combinations-for-ledger-entry--ledger-entry-lines) -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | ledger\_account\_id | The ledger account id | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | Updated 3 months ago * * * Ask AI --- # Filter Customers Use filters to retrieve only the customers that match your criteria (name, external reference, customer type, ledger account, etc.). Filtering customers is a common operation in integrations involving **CRM systems**, **billing tools**, **POS**, or **ERP platforms**. It helps reduce dataset size, improve performance, and reliably match customers across systems. This guide explains how to use the `filters[]` parameter with the **/customers** endpoint and lists all supported fields and operators. > 📘 > > **Reminder**: Filters use a JSON array passed as the filters query parameter. See Filter API Data for the general filtering syntax. How Filtering Works [](https://pennylane.readme.io/docs/filter-my-customers#how-filtering-works) ==================================================================================================== Customer filtering relies on the API’s standard `filters[]` format. Example filter array: JSON [\ { "field": "name", "operator": "start_with", "value": "Tes" }\ ] Example request: cURL GET /customers?filters=[{"field":"name","operator":"start_with","value":"Tes"}] You can combine multiple filters in a single request. **Common Use Cases** [](https://pennylane.readme.io/docs/filter-my-customers#common-use-cases) -------------------------------------------------------------------------------------------------- Developers typically filter customers to: * Search customers by name (autocomplete or lookup) * Match customers using an external reference from another system * Retrieve customers linked to a specific ledger account * Sync customer lists incrementally * Power customer selectors in internal tools Examples [](https://pennylane.readme.io/docs/filter-my-customers#examples) ============================================================================== Filter by customer name (autocomplete) [](https://pennylane.readme.io/docs/filter-my-customers#filter-by-customer-name-autocomplete) ---------------------------------------------------------------------------------------------------------------------------------------- cURL GET /customers?filters=[\ {"field":"name","operator":"start_with","value":"Tes"}\ ] Filter by external reference [](https://pennylane.readme.io/docs/filter-my-customers#filter-by-external-reference) ---------------------------------------------------------------------------------------------------------------------- cURL GET /customers?filters=[\ {"field":"external_reference","operator":"eq","value":"CRM-CUST-445"}\ ] Filter by customer type [](https://pennylane.readme.io/docs/filter-my-customers#filter-by-customer-type) ------------------------------------------------------------------------------------------------------------ cURL GET /customers?filters=[\ {"field":"customer_type","operator":"eq","value":"company"}\ ] Filter by multiple customer IDs [](https://pennylane.readme.io/docs/filter-my-customers#filter-by-multiple-customer-ids) ---------------------------------------------------------------------------------------------------------------------------- cURL GET /customers?filters=[\ {"field":"id","operator":"in","value":["cust_123","cust_456"]}\ ] Available Fields and Operators [](https://pennylane.readme.io/docs/filter-my-customers#available-fields-and-operators) ========================================================================================================================== | **Field** | **Description** | **Value Type** | **Operators** | | --- | --- | --- | --- | | `id` | Customer ID | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `customer_type` | Customer type (e.g. individual, company) | String | `eq`, `not_eq` | | `ledger_account_id` | Linked ledger account ID | String | `eq`, `not_eq` | | `name` | Customer name | String | `start_with` | | `external_reference` | External reference from another system | String | `eq`, `not_eq`, `in`, `not_in` | | `reg_no` | Registration number | String | `eq`, `not_eq`, `in`, `not_in` | > 💡 > > **Tip:** The `start_with` operator is ideal for search-as-you-type or autocomplete interfaces. Tips & Troubleshooting [](https://pennylane.readme.io/docs/filter-my-customers#tips--troubleshooting) ========================================================================================================= * **URL-encode the filter array** to avoid 400 errors. * Combine filters with pagination when retrieving large datasets (using the pagination mechanism supported by the endpoint). * Filtering by `name` is **prefix-based**, not a full-text search. * External references and registration numbers use **exact matching**. * Trim leading or trailing spaces in filter values. * Use `in` / `not_in` when syncing or reconciling multiple customers at once. * For incremental updates, combine filters with the **Changelog API**. Updated 24 days ago * * * * [Filter API Data](https://pennylane.readme.io/docs/setting-up-filters) * [Filter Suppliers](https://pennylane.readme.io/docs/filter-my-suppliers) * [List customers (company and individual)](https://pennylane.readme.io/reference/getcustomers) * [Track Data Changes with the API](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) Ask AI --- # Use Cursor-Based Pagination Our API uses cursor-based pagination to handle large collections of data efficiently for alpha index endpoints. How It Works [](https://pennylane.readme.io/docs/using-cursor-based-pagination#how-it-works) ------------------------------------------------------------------------------------------------ Each paginated response includes the results and pagination metadata: json { "items": [...], "has_more": true, "next_cursor": "eyJpZCI6MTAwfQ==" } Making Requests [](https://pennylane.readme.io/docs/using-cursor-based-pagination#making-requests) ------------------------------------------------------------------------------------------------------ ### Initial Request [](https://pennylane.readme.io/docs/using-cursor-based-pagination#initial-request) Make your first request without a cursor: `GET /api/external/v2/customer_invoices` You can specify the number of results per page using the limit parameter: `GET /api/external/v2/customer_invoices?limit=50` Note: The default limit is 20. Maximum limit varies by endpoint - refer to the specific endpoint documentation. ### Subsequent Pages [](https://pennylane.readme.io/docs/using-cursor-based-pagination#subsequent-pages) To get the next page, use the next\_cursor value from the previous response: `GET /api/external/v2/customer_invoices?cursor=eyJpZCI6MTAwfQ==&limit=50` Understanding the Response [](https://pennylane.readme.io/docs/using-cursor-based-pagination#understanding-the-response) ---------------------------------------------------------------------------------------------------------------------------- * `items`: Contains the current page of results * `has_more`: Indicates if more results are available * `next_cursor`: Used to retrieve the next page * Present and non-empty when more results exist * `null` when you've reached the end Important Notes [](https://pennylane.readme.io/docs/using-cursor-based-pagination#important-notes) ------------------------------------------------------------------------------------------------------ * Cursors are temporary and should not be stored for long-term use * Invalid cursors will result in a 400 Bad Request response * Using a consistent `limit` value across requests is recommended * Check each endpoint's documentation for its specific maximum limit Updated 3 months ago * * * Ask AI --- # Create a Downpayment Invoice **Import an invoice as a downpayment** * You need to set only one invoice line with the ledger account number 4191 and import is using the [Import an invoice endpoint](https://pennylane.readme.io/v2.0/reference/importcustomerinvoices) **Import the final invoice** * You need to set all your usual invoice lines * You need to add one negative invoice line with the same amount of your down payment with the ledger account number 4191 * You can then import the invoice using the [Import an invoice endpoint](https://pennylane.readme.io/v2.0/reference/importcustomerinvoices) You can also use this to create a downpayment invoice using the [create endpoint](https://pennylane.readme.io/v2.0/reference/postcustomerinvoices) . However, this will not show as a downpayment in the UI but as a regular invoice Updated 3 months ago * * * Ask AI --- # Public Roadmap > 📘 > > ### > > Stay tuned by subscribing to our [Changelog RSS feed](https://pennylane.readme.io/v2.0/changelog.rss) > > [](https://pennylane.readme.io/docs/api-public-roadmap#stay-tuned-by-subscribing-to-our-changelog-rss-feed) > > 💡Tutorial on how to set it up [here](https://pennylane.readme.io/docs/stay-updated-with-our-api-changes) You can check our Public API Roadmap [here](https://pennylane-hq.notion.site/8dece65ef66a4d8db66eb27f5e25c88c?v=1602276c03bf803e81a4000c99db9c3a) in 🇫🇷 The new features / changes / improvements can be tracked on our changelog [here](https://pennylane.readme.io/changelog) Updated 3 months ago * * * Ask AI --- # Automate Payment Matching How does it work? [](https://pennylane.readme.io/docs/automating-payment-matching#how-does-it-work) ------------------------------------------------------------------------------------------------------- If you have a payment to link with the invoice you are creating / importing, it is possible to automatically reconciliate the invoice with this transaction. The only condition is to be able to give us a **unique id** so that we know which transaction to match with the invoice. To do so, you need to use the **transaction\_reference** object. ![](https://files.readme.io/957add0dd22c5b3b8a56985ac602b2e382faaedf3c8531f759294268abbd00eb-Capture_decran_2025-10-15_a_18.01.36.png) If you don't want to use the automatic matching, please leave all the fields of the transaction\_reference object empty. What are the use cases we currently support? [](https://pennylane.readme.io/docs/automating-payment-matching#what-are-the-use-cases-we-currently-support) ------------------------------------------------------------------------------------------------------------------------------------------------------------- We currently support automatic matching with the invoice number and Stripe payment ID. If you would like to use another unique ID, do not hesitate to contact us: we will assess together if we can add your use case. \# 1: Invoice number (with any bank) [](https://pennylane.readme.io/docs/automating-payment-matching#-1-invoice-number-with-any-bank) ----------------------------------------------------------------------------------------------------------------------------------------- To use the invoice number as a key, you first need to make sure that your client will write the invoice number in the label of the transfer (or in the label of the direct debit). As the invoice number is unique, we will then be able to automatically match the invoice with the bank transaction we'll received on the other hand. In this case, you need to fill the transaction\_reference object as follows: transaction\_reference banking_provider: 'bank' provider_field_name: 'label' provider_field_value: 'invoice_number' This way, we will look into all your bank transactions and detect which transaction has a label which contain the invoice number of the sent invoice. It's not a problem if the label does not only contrains the invoice number. You can also give us any other unique reference that will be in the label. For example: transaction\_reference banking_provider: 'bank' provider_field_name: 'label' provider_field_value: my_unique_reference_1234 \# 2: Stripe [](https://pennylane.readme.io/docs/automating-payment-matching#-2-stripe) ------------------------------------------------------------------------------------------- If you are creating the invoice after the Stripe paiement has been initiated, then you can communicate us the payment\_id of the Stripe transaction. As this payment\_id is unique, we will then be able to automatically match the invoice with the Stripe transaction we'll receive on the other hand. In this case, you need to fill the transaction\_reference object as follows: transaction\_reference banking_provider: 'stripe' provider_field_name: 'payment_id' provider_field_value: enter the Stripe paiement id (something similar to pi_XXXX) You can also fill the transaction\_reference object as follows to do the matching with a charge\_id: transaction\_reference banking_provider: 'stripe' provider_field_name: 'charge_id' provider_field_value: enter the Stripe charge id (something similar to ch_XXXX) In case you use Stripe aggregated reports, you can, **afterwards** link your invoices to the aggregated transaction by using this reference: transaction\_reference banking_provider: 'stripe' provider_field_name: 'report_id' provider_field_value: enter the Stripe report id (something similar to frr_XXXX) \# 3: GoCardless [](https://pennylane.readme.io/docs/automating-payment-matching#-3-gocardless) --------------------------------------------------------------------------------------------------- If you are creating the invoice after the GoCardless paiement has been initiated, then you can communicate us the payment\_id of the GoCardless transaction. As this payment\_id is unique, we will then be able to automatically match the invoice with theGocardless transaction we'll receive on the other hand. In this case, you need to fill the transaction\_reference object as follows: transaction\_reference banking_provider: 'gocardless' provider_field_name: 'payment_id' provider_field_value: enter the Gocardless paiement id (something similar to PM00XXXXXXX) Updated 3 months ago * * * Ask AI --- # Rate Limiting in API v2 Pennylane API relies on rate limits to ensure stability and reliability. Rate limiting is enabled on both production and sandbox environments and all endpoints are affected. > 📘 > > ### > > The rate limiting is applied at the token level. If you have an OAuth app, it will be applied on each generated token from your OAuth app. If you have a developer token, it will be applied directly on your token. > > [](https://pennylane.readme.io/docs/rate-limiting-1#the-rate-limiting-is-applied-at-the-token-level-if-you-have-an-oauth-app-it-will-be-applied-on-each-generated-token-from-your-oauth-app-if-you-have-a-developer-token-it-will-be-applied-directly-on-your-token) You are allowed to make 5 requests per second. If you go over this limit, you will receive a 429 HTTP Error : Text Rate limit exceeded. Please retry in X seconds. Updated 3 months ago * * * Ask AI --- # Track Data Changes with the API Changelog Endpoints [](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api#changelog-endpoints) ========================================================================================================================= Changelog endpoints allow you to track changes (creation, updates, deletion) on specific resources in near real-time. They are particularly useful for maintaining data synchronization between your system and Pennylane. Available Changelog Endpoints [](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api#available-changelog-endpoints) --------------------------------------------------------------------------------------------------------------------------------------------- Changes are available for the following resources: * Customer Invoices (`/api/external/v2/changelogs/customer_invoices`) * Supplier Invoices (`/api/external/v2/changelogs/supplier_invoices`) * Customers (`/api/external/v2/changelogs/customers`) * Suppliers (`/api/external/v2/changelogs/suppliers`) * Products (`/api/external/v2/changelogs/products`) * Ledger Entry Lines (`/api/external/v2/changelogs/ledger_entry_lines`) * Transactions (`/api/external/v2/changelogs/transactions`) When to Use Changelog Endpoints [](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api#when-to-use-changelog-endpoints) ------------------------------------------------------------------------------------------------------------------------------------------------- Use changelog endpoints when you need to: * Keep your local copy of Pennylane data up to date * React to specific changes in resources (e.g., when an invoice is created) * Build an audit trail of changes * Implement incremental synchronization instead of fetching all resources repeatedly How It Works [](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api#how-it-works) ----------------------------------------------------------------------------------------------------------- 1. **Initial Sync**: * Without a `start_date` parameter, the endpoint returns changes starting from 4 weeks ago * You can specify a more recent `start_date` if you only need recent changes * Note that you cannot request changes older than 4 weeks 2. **Processing Changes**: * Changes are returned in chronological order (oldest first) * Each change contains the resource ID and the type of operation * Use the resource ID to fetch complete data from the corresponding endpoint 3. **Pagination**: * Results are paginated with a default limit of 20 items (max 1000) * Use the `next_cursor` to fetch subsequent pages when `has_more` is true * The cursor is only valid for the current request session * You can read more about [how to use cursor-based pagination](https://dash.readme.com/project/pennylane/v1.0/docs/using-cursor-based-pagination) Important Notes [](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api#important-notes) ----------------------------------------------------------------------------------------------------------------- * Changes are retained for 4 weeks * Results are paginated with a default limit of 20 items (max 1000) * The `operation` field indicates the type of change: * `insert`: Resource created * `update`: Resource modified * `delete`: Resource deleted * You cannot use both `start_date` and `cursor` in the same request Best Practices [](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api#best-practices) --------------------------------------------------------------------------------------------------------------- 1. **Regular Polling**: * Poll the changelog endpoint at regular intervals (e.g., every few hours) * Store the `processed_at` of the last change you've processed * On next poll, use this timestamp as `start_date` to get only new changes 2. **Processing Changes**: * Process changes in chronological order * Use pagination (via `cursor`) only to navigate through a large result set * Once all changes are processed for a request, start a new request with an updated `start_date` 3. **Resource Fetching**: When you receive changes from the changelog endpoints, you'll need to fetch the complete resource data. Here's how to do it efficiently: 1. **Using Resource IDs**: * Each change event includes a resource `id` * Use these IDs to fetch the current state of resources from their respective endpoints * For deleted resources (`operation: "delete"`), the resource will no longer be available 2. **Batch Fetching**: * Instead of fetching each resource individually, collect multiple IDs * Use the `filter` parameter with the `in` operator on resource endpoints * Example filter: `[{"field": "id", "operator": "in", "value": [123, 456, 789]}]` * This reduces the number of API calls and improves performance 3. **Available Resource Endpoints**: * Customer Invoices: `/api/external/v2/customer_invoices` * Supplier Invoices: `/api/external/v2/supplier_invoices` * Products: `/api/external/v2/products` * Customers: `/api/external/v2/customers` * Suppliers: `/api/external/v2/suppliers` 4. **Handling Missing Resources**: * Some resources might have been deleted since the change was recorded * Implement proper error handling for these cases Updated 3 months ago * * * Ask AI --- # How to reach out to us If you are a customer in need of support, please reach out to our support teams through the usual process (chat or account manager). If you are a partner in need of support, please use your Zendesk access. If you don't have one, request it to [\[email protected\]](https://pennylane.readme.io/cdn-cgi/l/email-protection#33435241475d5641405b5a43407343565d5d4a5f525d561d505c5e) . Updated 3 months ago * * * Ask AI --- # API Changelog API Changelog [](https://pennylane.readme.io/docs/stay-updated-with-our-api-changes#api-changelog) ------------------------------------------------------------------------------------------------------ We maintain a dedicated changelog page where you can find a complete history of updates and changes to our API: [View our API Changelog](https://pennylane.readme.io/changelog) The changelog is regularly updated with detailed information about: * New endpoint releases * Breaking changes * Deprecation notices * Bug fixes * Feature enhancements RSS Feed [](https://pennylane.readme.io/docs/stay-updated-with-our-api-changes#rss-feed) -------------------------------------------------------------------------------------------- For automatic notifications of these changes, we provide an RSS feed that delivers the same changelog updates directly to you: 1. Subscribe to our feed: [https://pennylane.readme.io/changelog.rss](https://pennylane.readme.io/changelog.rss) 2. Choose your preferred RSS reader: * [Feedly](https://feedly.com/) * [Inoreader](https://www.inoreader.com/) * Browser extensions (like [RSS Feed Reader for Chrome](https://chromewebstore.google.com/detail/rss-feed-reader/pnjaodmkngahhkoihejjehlcdlnohgmp?hl=en) ) * Email services that support RSS (like [Mailbrew](https://mailbrew.com/) ) * Desktop applications (like [NetNewsWire](https://netnewswire.com/) for Mac) Subscribe via Slack [](https://pennylane.readme.io/docs/stay-updated-with-our-api-changes#subscribe-via-slack) ------------------------------------------------------------------------------------------------------------------ Keep your development team in the loop by adding our RSS feed to a Slack channel: 1. In your Slack workspace, click the + button next to "Apps" in the sidebar 2. Search for and add the RSS app (or use a third-party app like "RSS for Slack") 3. Once installed, use the slash command to subscribe: `/feed subscribe https://pennylane.readme.io/changelog.rss` 4. Choose which channel should receive the updates 5. Your team will now automatically receive notifications in Slack whenever we update our API Benefits of Subscribing [](https://pennylane.readme.io/docs/stay-updated-with-our-api-changes#benefits-of-subscribing) -------------------------------------------------------------------------------------------------------------------------- * Never miss critical API changes * Plan your integration updates ahead of time * Stay informed without checking our documentation manually * Receive the same information as soon as it's published Updated 3 months ago * * * Ask AI --- # Import vs Create a Customer Invoice Importing a customer invoice [](https://pennylane.readme.io/docs/import-vs-create-a-customer-invoice-whats-the-difference#importing-a-customer-invoice) =========================================================================================================================================================== You can import an existing customer invoice when the invoice has been made in another tool. This means you need to provide the file of the invoice on top of the data needed to create the invoice. The file needs to be uploaded using File attachments scope creation endpoint. You can then pass on the id of the file attachment in the import endpoint. When you import an invoice, you can choose to send it to the Pennylane's inbox section by selecting `import_as_incomplete`. This also means that the invoice is sent as `to be treated` on the accounting side. > 🚧 > > ### > > The import endpoint does not use OCR. This means you need to provide all the information of the invoice as well as the file. If you want to upload invoices and use Pennylane's OCR, you can try out [the mail import feature](https://help.pennylane.com/fr/articles/185021-importer-des-factures-par-transfert-d-e-mail) > . > > [](https://pennylane.readme.io/docs/import-vs-create-a-customer-invoice-whats-the-difference#the-import-endpoint-does-not-use-ocr-this-means-you-need-to-provide-all-the-information-of-the-invoice-as-well-as-the-file-if-you-want-to-upload-invoices-and-use-pennylanes-ocr-you-can-try-out-the-mail-import-feature) Creating a customer invoice [](https://pennylane.readme.io/docs/import-vs-create-a-customer-invoice-whats-the-difference#creating-a-customer-invoice) ========================================================================================================================================================= You can use this endpoint to create a new invoice that will use Pennylane's invoice editor. This means that the endpoint also generates the PDF of the file. You can choose the template of your choice when creating the customer invoice. Unless it is in draft status, the customer invoice is considered finalized. If you want to cancel it, you need to create a credit note. > 📘 > > ### > > In both cases, you need to create all objects needed for the invoice beforehand : customer, product, categories, etc. and link them to the invoice with their id. > > [](https://pennylane.readme.io/docs/import-vs-create-a-customer-invoice-whats-the-difference#in-both-cases-you-need-to-create-all-objects-needed-for-the-invoice-beforehand--customer-product-categories-etc-and-link-them-to-the-invoice-with-their-id) Updated about 1 month ago * * * Ask AI --- # Glossary | API term | French 🇫🇷 | Meaning | | --- | --- | --- | | customer invoice | facture de vente | Invoice billed to a client | | credit note | avoir | Cancelled customer invoice | | estimate | devis | Quote | | ledger account | compte comptable | Accounting account linked to an object. For instance, product ledger accounts start with a 7, while customers start with a 4. This is regulated by the french Plan Comptable Général | | categories | catégories/axes analytiques | Analytical assignation of an invoice, transaction, ledger entry,... | | ledger entry | pièce comptable | Accounting entry containing multiple entry lines | | ledger entry line | ligne d'écriture | Smallest element of a ledger entry. Entry lines need to be balanced (credit=debit) in french accounting. | | supplier invoice | facture d'achats | Invoice of purchased items, billed by a supplier | | trial balance | balance générale | Accounting balance containing all ledger accounts of a company + their balance | | fiscal years | années fiscales | Years within which accounting happens for a particular exercise | Updated 3 months ago * * * Ask AI --- # Understanding VAT Rates on Ledger Accounts Introduction [](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts#introduction) ------------------------------------------------------------------------------------------------------ In accounting systems, ledger account are essential for tracking ledger entries. Each ledger entry line is recorded in a specific ledger account, which helps businesses manage their accounting efficiently. Value Added Tax (VAT) is a crucial component of this process, as it affects the financial statements and tax obligations of a business. Identifying Ledger Accounts by VAT Rate [](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts#identifying-ledger-accounts-by-vat-rate) ------------------------------------------------------------------------------------------------------------------------------------------------------------ For every applicable VAT rate, a distinct ledger account item is typically assigned with a unique ID. This ensures that ledger entries are recorded accurately based on the VAT rate applied. For instance, you might have separate ledger accounts items for VAT rates of 0%, 5%, and 10%. Practical Example [](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts#practical-example) ---------------------------------------------------------------------------------------------------------------- To create a ledger entry line on ledger account number 706000000009 with a VAT rate of 20%, here are the steps to follow : 1. Check that the ledger account exists on Pennylane using this [endpoint](https://pennylane.readme.io/reference/getledgeraccounts) and this filter : `[{"field": "number", "operator": "start_with", "value": "706000000009"}]` 2. If it exists, several items will come up in the response for this ledger account : { "total_pages": 1, "current_page": 1, "per_page": 20, "total_items": 3, "items": [ \ { \ "id": 1326475509, \ "number": "706000000009", \ "label": "Prestations de services", \ "vat_rate": "FR_200", \ "country_alpha2": "any", \ "enabled": true \ }, \ { \ "id": 1326475560, \ "number": "706000000009", \ "label": "Prestations de services", \ "vat_rate": "any", \ "country_alpha2": "any", \ "enabled": true \ }, \ { \ "id": 1326475596, \ "number": "706000000009", \ "label": "Prestations de services", \ "vat_rate": "exempt", \ "country_alpha2": "any", \ "enabled": true \ } \ ] } 3. In order to send ledger entry lines with 20% VAT rate, the ledger\_account\_id `1326475509`must be selected here as it is associated with a `FR_200` vat\_rate value (20%) 4. If the ledger account doesn't exist in Pennylane, you can create it using this [endpoint](https://pennylane.readme.io/reference/postledgeraccounts) and indicate the VAT rate associated using the `vat_rate` field Consequences [](https://pennylane.readme.io/docs/handle-vat-rates-by-ledger-accounts#consequences) ------------------------------------------------------------------------------------------------------ If a ledger account with `any` VAT rate is used instead of a precise VAT rate (like `FR_200` for example), a `N/A` value will be indicated on Pennylane interface (in red below) whereas a precise VAT rate will be properly displayed (for example `FR_200` / 20%, in green below) : ![](https://files.readme.io/fdfc8dfbd5ba2ec1acd7f7fabc545be54543f58e5ba320c6ad66cf824d198dc7-Capture_decran_2025-07-31_a_10.23.53.png) Updated about 1 month ago * * * Ask AI --- # Invoicing Management Invoicing is a key part of how businesses operate - and with Pennylane’s API, you can automate the entire flow, from invoice creation to accounting synchronization. This guide helps you choose the right invoicing workflow for your integration - whether you’re importing existing invoices or creating them directly in Pennylane. You will learn how to: * Connect your billing or procurement system to Pennylane * Import invoices from other systems, or create them directly via the API * Keep your accounting data automatically synchronized What You Can Do with the API [](https://pennylane.readme.io/docs/invoicing-use-cases#what-you-can-do-with-the-api) ====================================================================================================================== Pennylane’s API lets you manage both supplier and customer invoices programmatically. You can: * **Import existing invoices** (PDFs or structured data) into Pennylane * **Create new invoices** directly in Pennylane * **Categorize, send, and reconcile** invoices automatically * **Sync invoice data** across your systems (CRM, ERP, POS, etc.) This flexibility lets you integrate Pennylane into your invoicing workflows — for example, from a POS, billing, or procurement system. Typical Invoicing Scenarios [](https://pennylane.readme.io/docs/invoicing-use-cases#typical-invoicing-scenarios) ==================================================================================================================== | **Scenario** | **Example Use Case** | | --- | --- | | **POS or ERP Integration** | Automatically create customer invoices when a sale is made. | | **Supplier Invoice Automation** | Import supplier invoices from purchasing or expense management tools. | | **CRM Integration** | Sync customer invoice data from your CRM for unified financial tracking. | | **Invoice Capture Automation** | Import scanned or OCR-processed supplier invoices directly into Pennylane. | Available Tutorials [](https://pennylane.readme.io/docs/invoicing-use-cases#available-tutorials) ==================================================================================================== | **Tutorial** | **Description** | | --- | --- | | [Import a Supplier Invoice via API](https://pennylane.readme.io/docs/importing-a-supplier-invoice) | Import supplier invoices received from external tools or vendors and automatically record them in Pennylane. | | [Import a Customer Invoice via API](https://pennylane.readme.io/docs/importing-a-customer-invoice) | Import customer invoices generated in another system (e.g., ERP, CRM, or billing app). | | [Create a Customer Invoice via API](https://pennylane.readme.io/docs/creating-a-customer-invoice) | Create invoices directly in Pennylane using templates, products, and VAT configurations. | > 💡 > > All invoicing operations are immediately reflected in accounting once validated in Pennylane. Commonly Used Endpoints [](https://pennylane.readme.io/docs/invoicing-use-cases#commonly-used-endpoints) ============================================================================================================ You will use the following endpoints across invoicing workflows: | **Endpoint** | **Usage** | | --- | --- | | `/customer_invoices` | Create, import, or list customer invoices | | `/supplier_invoices` | Import or list supplier invoices | | `/file_attachments` | Upload and attach invoice PDFs | | `/categories` | Categorize invoices for reporting | | `/ledger_accounts` | Retrieve account mappings | | `/products` | Link product lines to invoices | > 📘 > > See the full API reference for endpoint details and payload specifications. Required Scopes [](https://pennylane.readme.io/docs/invoicing-use-cases#required-scopes) ============================================================================================ Most invoicing operations require these scopes: | **Scope** | **Purpose** | | --- | --- | | `customer_invoices:all` | Create, import, and send customer invoices | | `supplier_invoices:all` | Import and manage supplier invoices | | `file_attachments:all` | Upload and attach files to invoices | | _(Optional)_ `categories:all` | Categorize invoices | | _(Optional)_ `ledger_accounts:readonly` | Retrieve accounting mappings | | _(Optional)_ `products:readonly` | Access product catalog | > 📘 > > Refer to the Understand Scopes page for the complete list. Next Steps [](https://pennylane.readme.io/docs/invoicing-use-cases#next-steps) ================================================================================== Choose the workflow that matches your integration: ➡️ If you already generate invoices elsewhere → **Import them via the API** ➡️ If you want Pennylane to generate invoices → **Create them directly** ➡️ Then, optionally → **Categorize, email, or reconcile** your invoices automatically Updated about 1 month ago * * * * [Supported Use Cases](https://pennylane.readme.io/docs/supported-use-cases) * [Send Documents by Email](https://pennylane.readme.io/docs/sending-documents-by-email) * [Track Data Changes with the API](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) * [Use Cursor-Based Pagination](https://pennylane.readme.io/docs/using-cursor-based-pagination) Ask AI --- # String Character Limits In general, the limit of string characters is 2000. The few exceptions will be documented directly in the attribute in our readme documentation. Updated about 1 month ago * * * Ask AI --- # Send Documents by Email With this API you can send by emails the documents you've just created. How does it work? [](https://pennylane.readme.io/docs/sending-documents-by-email#how-does-it-work) ------------------------------------------------------------------------------------------------------ 1. [Create](https://pennylane.readme.io/v2.0/reference/postcustomerinvoices) (or [import](https://pennylane.readme.io/reference/customer_invoices-import) ) the document 2. Get the id of the document in the response 3. Send the email to the client using [this endpoint](https://pennylane.readme.io/v2.0/reference/sendbyemailcustomerinvoice) Sending those emails requires that the PDF file for the document has been generated (this process can take a few minutes), so if you just created the invoice / quote in our system, we may return a 409 error. You should retry the request in a few minutes - if you receive a 204 response, that means that the email is on its way. Recipients [](https://pennylane.readme.io/docs/sending-documents-by-email#recipients) ----------------------------------------------------------------------------------------- To send the email you can specify one or more recipients email addresses. If no recipients are specified, the recipients used will be the email addresses set on the customer of the document. If the email address of the customer is missing, you will receive an error message telling you to first specify the email address. ![](https://files.readme.io/8671840-email_address.png "email address.png") At the moment, it is not possible to specify another email address in cc or bcc. The emails will be sent with the following address: [\[email protected\].](https://pennylane.readme.io/cdn-cgi/l/email-protection#94f2f5f7e0e1e6f5e0fdfbfad4e4f1fafaedf8f5faf1baf7fbf9ba) If your client answers the email, their answer will be automatically transferred to the email address you've specified in the Paramètres > "Facturation clients" in the platform. ![](https://files.readme.io/0ba2f80-sender_email_address.png "sender email address.png") Email templates [](https://pennylane.readme.io/docs/sending-documents-by-email#email-templates) --------------------------------------------------------------------------------------------------- At the moment, it is not possible for you to select or create email templates with the API. We use by default the default invoice and quote templates you already have in the platform. Depending of the billing language of the customer, we will either select the French default one or the English default one. You can see them in the Parameters > "Facturation clients" > "Emails" . Feel free to modify the content of those templates if needed. Updated 3 months ago * * * Ask AI --- # Payment vs Matched Transaction Payments [](https://pennylane.readme.io/docs/what-is-the-difference-between-a-payment-and-a-matched-transaction#payments) ============================================================================================================================= A payment in Pennylane refers to an invoice that was paid using Pennylane's means of payment : * With Qonto * With the payment link set up in the invoice * With Gocardless * With Stripe In this case, the invoice is automatically matched to the payment. Matched transaction [](https://pennylane.readme.io/docs/what-is-the-difference-between-a-payment-and-a-matched-transaction#matched-transaction) =================================================================================================================================================== In case the payment happens outside of Pennylane (for instance by bank transfer), the user can match the transaction with the invoice, either on Pennylane's app, or using the API (see this [guide](https://pennylane.readme.io/v2.0/docs/automating-payment-matching) ). In this case, the invoice is matched manually. Updated about 1 month ago * * * Ask AI --- # Filter Customer Invoices Use filters to retrieve only the customer invoices that match your criteria (date range, customer, invoice number, subscription, etc.). Filtering helps reduce API call volume, improve performance, and retrieve exactly the dataset your integration needs. This guide shows how to use the `filters[]` parameter with the `/customer_invoices` endpoint and lists all supported fields and operators. > 📘 > > **Reminder:** Filters use a JSON array of objects passed as the `filters` query parameter. > See [Filter API Data](https://pennylane.readme.io/update/docs/setting-up-filters) > for the general filtering syntax. How Filtering Works [](https://pennylane.readme.io/docs/filter-my-customer-invoices#how-filtering-works) ============================================================================================================ Customer invoice filtering relies on the API’s standard `filters[]` format: JSON [\ { "field": "customer_id", "operator": "eq", "value": "cust_123" },\ { "field": "date", "operator": "gteq", "value": "2024-01-01" }\ ] You can combine multiple filters at once. Filters must be **URL-encoded** when passed as query parameters. Example request: cURL GET /customer_invoices?filters=[{"field":"customer_id","operator":"eq","value":"cust_123"}] Common Use Cases [](https://pennylane.readme.io/docs/filter-my-customer-invoices#common-use-cases) ====================================================================================================== Here are the most frequent patterns used by integrators: * Retrieve all invoices for a specific customer * Get invoices issued after a certain date * Search invoices matching one or several invoice numbers * Retrieve invoices associated with a billing subscription * Filter invoices for BI or reconciliation workflows Examples [](https://pennylane.readme.io/docs/filter-my-customer-invoices#examples) ====================================================================================== Filter by customer [](https://pennylane.readme.io/docs/filter-my-customer-invoices#filter-by-customer) ---------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[{"field":"customer_id","operator":"eq","value":"cust_987"}] Filter by date range [](https://pennylane.readme.io/docs/filter-my-customer-invoices#filter-by-date-range) -------------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"date","operator":"gteq","value":"2024-01-01"},\ {"field":"date","operator":"lteq","value":"2024-03-31"}\ ] Filter by several invoice numbers [](https://pennylane.readme.io/docs/filter-my-customer-invoices#filter-by-several-invoice-numbers) ---------------------------------------------------------------------------------------------------------------------------------------- cURL GET /customer_invoices?filters=[\ {"field":"invoice_number","operator":"in","value":["INV-2024-001","INV-2024-002"]}\ ] Available Fields and Operators [](https://pennylane.readme.io/docs/filter-my-customer-invoices#available-fields-and-operators) ================================================================================================================================== | **Field** | **Description** | **Value Type** | **Operators** | | --- | --- | --- | --- | | `id` | Customer invoice ID | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `customer_id` | Customer ID | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `date` | Invoice issue date (ISO 8601) | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `invoice_number` | Invoice reference | String | `eq`, `not_eq`, `in`, `not_in` | | `billing_subscription_id` | Linked subscription | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `estimate_id` | Linked estimate (quote) | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq`, `in`, `not_in` | | `category_id` | Assigned category | String | `in` | > 💡 > > **Tip:** date filters must use ISO 8601 format `(YYYY-MM-DD)`. Tips & Troubleshooting [](https://pennylane.readme.io/docs/filter-my-customer-invoices#tips--troubleshooting) ================================================================================================================= * **URL-encode the filter array** to avoid 400 Bad Request errors. * Combine filters with pagination when retrieving large datasets (using the pagination mechanism supported by the endpoint). * Use **ISO date strings** for all date filters. * Use `in` and `not_in` for bulk retrievals (e.g., list of customers or invoice numbers). * Filtering by `invoice_number` is **exact-match**, not a text search. * To retrieve “recent changes”, prefer filtering by `updated_at` (if available on the endpoint) or use the **Changelog API**. Updated 25 days ago * * * * [Filter API Data](https://pennylane.readme.io/docs/setting-up-filters) * [Filter Supplier Invoices](https://pennylane.readme.io/docs/filter-my-supplier-invoices) * [Accounting Management](https://pennylane.readme.io/docs/accounting-use-cases) * [List customer invoices](https://pennylane.readme.io/reference/getcustomerinvoices) * [Track Data Changes with the API](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) Ask AI --- # Migrate from API v1 to v2 > ❗️ > > ### > > Depreciation strategy > > [](https://pennylane.readme.io/docs/api-v2-vs-v1#depreciation-strategy) > > **The V1 will be completely depreciated by end of 2025. The V2 is now the official stable version.** Recognize the version you're using [](https://pennylane.readme.io/docs/api-v2-vs-v1#recognize-the-version-youre-using) ========================================================================================================================== 💡To know on which version you are, have a look at the URL your code is using : * If it's V1, the path should contain /v1 * if it's V2, the path should contain /v2 If you don't have access to the URL, look at your token generated in the connectivity page : * If it has the following scopes, then it's V2 ⬇️ * Écritures comptables * Balance générale * Exercices fiscaux * Export Grand Livre Analytique * Export Fichier des Écritures Comptables * Modèles de factures * Transactions * Comptes bancaires * Fichiers DMS * Catégories * Clients * Produits * Factures fournisseurs * Fournisseurs * Factures client * Fichiers * Mandats clients * Devis * Documents commerciaux * Abonnements de facturation * if it has the following scopes, then it's v1 ⬇️ * Analytique et reporting * Gestion des ventes * Gestion des fournisseurs If you're using an OAuth App, here are the [v1 scopes](https://pennylane.readme.io/v1.0/docs/v1-scopes) and the [v2 scopes](https://pennylane.readme.io/docs/v2-scopes) . What changes between v1 and v2 ? [](https://pennylane.readme.io/docs/api-v2-vs-v1#what-changes-between-v1-and-v2-) ====================================================================================================================== API v1 IDs vs. API v2 IDs [](https://pennylane.readme.io/docs/api-v2-vs-v1#api-v1-ids-vs-api-v2-ids) -------------------------------------------------------------------------------------------------------- The new API V2 relies exclusively on internal IDs to expose and retrieve any resources (product, customer, etc.). For instance, you cannot retrieve a product via its source\_id but only via its internal id. To help you migrate from V1 to V2, all the resources in the V1 response payloads now include an additional v2\_id attribute so you can map the old IDs to the new IDs. More granular scopes 🔐 [](https://pennylane.readme.io/docs/api-v2-vs-v1#more-granular-scopes-) --------------------------------------------------------------------------------------------------- In the v2, we’ve introduced more granular scopes to ensure your integration accesses the adequate data from a Pennylane Company. Each scope now comes with its readonly counterpart, which is the preferred scope if your app only needs to read information. **Example** `customer_invoices` in the v1 is now broken down into multiple scopes : * `customer_invoices` * read & write * `products` * read & write * `customers` * read & write * `file_attachements` * read & write 💡 Add the new [V2 scopes](https://pennylane.readme.io/docs/v2-scopes) to your OAuth app and re-authenticate it to be able to use V2 endpoints ! New pagination [](https://pennylane.readme.io/docs/api-v2-vs-v1#new-pagination) ----------------------------------------------------------------------------------- To bring more performance, we've changed the pagination system. Read [here](https://pennylane.readme.io/v2.0/docs/using-cursor-based-pagination) our dedicated guide. Creating ressources separately [](https://pennylane.readme.io/docs/api-v2-vs-v1#creating-ressources-separately) ------------------------------------------------------------------------------------------------------------------- In the v1, it was possible to create multiple types of objects in a single call, which was problematic for performance & debugging issues, and led to many errors. In the v2, each object has to be created separately, through a dedicated endpoint. This also is enforced by the granularity of scopes. We make sure that ressources can be created only if you or the third party you've given access to has a scope authorized. **Example** * In the v1, it was possible to create a customer while creating a customer\_invoice. * In the v2, it is now mandatory to first create the customer through the customer creation endpoint, and then pass on its ID in the customer creation endpoint. Renaming attributes & ressources [](https://pennylane.readme.io/docs/api-v2-vs-v1#renaming-attributes--ressources) ---------------------------------------------------------------------------------------------------------------------- Some attributes & ressources have been renamed ⬇️ * `plan_item` -> `ledger_account` * `customer_validation_needed` -> `import_as_incomplete` * `Estimates` -> `Quotes` Managing file attachments [](https://pennylane.readme.io/docs/api-v2-vs-v1#managing-file-attachments) --------------------------------------------------------------------------------------------------------- In the API V1, to attach a file when importing a customer invoice, you had to provide either the file field with the base64 content of the file or `file_url` with the URL to download it. In the V2, this file is a dedicated resource called a `file_attachment`. So, to import a customer invoice with a file, you first need to upload a file attachment, get the ID, and use that ID in the import customer invoice endpoint with the `file_attachment_id` field. Exposing nested collections in the response body in V2 [](https://pennylane.readme.io/docs/api-v2-vs-v1#exposing-nested-collections-in-the-response-body-in-v2) ------------------------------------------------------------------------------------------------------------------------------------------------------------------- In the API V2, whenever we have to expose a nested collection in a response, such as `invoice_lines` of a `customer_invoice`, we will provide a link towards the collection instead of an array/list of invoice\_line ids. Standardized response body in V2 [](https://pennylane.readme.io/docs/api-v2-vs-v1#standardized-response-body-in-v2) ----------------------------------------------------------------------------------------------------------------------- We have standardized the response body in V2, meaning that a data model (such as customer\_invoice, for example) will always have the same response body attributes exposed, regardless of the endpoint (show, list). Changelogs [](https://pennylane.readme.io/docs/api-v2-vs-v1#changelogs) --------------------------------------------------------------------------- We've added changelog endpoints on most common ressources (`customer_invoices`, `supplier_invoices`, `suppliers`, `customers`, `suppliers`, etc.) to allow you to list the events and changes that happened over a ressource. Read [here](https://pennylane.readme.io/docs/tracking-data-changes-with-pennylane-api) our full guide. More filters [](https://pennylane.readme.io/docs/api-v2-vs-v1#more-filters) ------------------------------------------------------------------------------- To be more performant while querying endpoints, we've added new filters in the V2. Look at our guide to setting up filters [here](https://pennylane.readme.io/v2.0/docs/setting-up-filters) . Floats are handled as strings [](https://pennylane.readme.io/docs/api-v2-vs-v1#floats-are-handled-as-strings) ----------------------------------------------------------------------------------------------------------------- In the V2, to avoid errors when it comes to float values, it is now mandatory to pass them on as strings. In the V1 : ![](https://files.readme.io/485c11dbf8f9d56b55e5fa3a4040b75202ef2036dae8181a13d2ef06359d5b7c-image.png) In the V2 : ![](https://files.readme.io/9458d42f619459b8845ea42a5431e5b63664712302f6c19434740d49de733332-image.png) Updated 3 months ago * * * Ask AI --- # V1 Scopes [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) Required scopes per endpoint [](https://pennylane.readme.io/v1.0/docs/v1-scopes#required-scopes-per-endpoint) ----------------------------------------------------------------------------------------------------------------- Here's our mapping of API endpoints and their required scopes: | Scopes | Endpoints | | --- | --- | | accounting | * Categories
\- Category Groups
\- Enums
\- User profile | | customer\_invoices | * Billing Subscriptions
\- Customer Invoices
\- Customers
\- Enums
\- Estimates
\- Plan Items
\- Products
\- User profile | | supplier\_invoices | * Enums
\- Plan Items
\- Supplier Invoices
\- Suppliers
\- User profile | Updated 4 months ago * * * Ask AI --- # V1 Rate limiting [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) Pennylane API relies on rate limits to ensure stability and reliability. Rate limiting is enabled on both production and sandbox environments and all endpoints are affected. > 📘 > > ### > > The rate limiting is applied at the token level. If you have an OAuth app, it will be applied on each generated token from your OAuth app. If you have a developer token, it will be applied directly on your token. > > [](https://pennylane.readme.io/v1.0/docs/rate-limiting-1#the-rate-limiting-is-applied-at-the-token-level-if-you-have-an-oauth-app-it-will-be-applied-on-each-generated-token-from-your-oauth-app-if-you-have-a-developer-token-it-will-be-applied-directly-on-your-token) You are allowed to make 5 requests per second. If you go over this limit, you will receive a 429 HTTP Error : Text Rate limit exceeded. Please retry in X seconds. Updated 4 months ago * * * Ask AI --- # Testing my connection [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) You can run a quick test to make sure your connection is up and running by calling this [endpoint](https://pennylane.readme.io/reference/get-me-2) . Updated 4 months ago * * * Ask AI --- # Sending estimates / invoices by email [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) With this API you can send by emails the invoices or estimates you've just created. How does it work? [](https://pennylane.readme.io/v1.0/docs/how-to-send-estimates-invoices-by-emails#how-does-it-work) ------------------------------------------------------------------------------------------------------------------------- Send invoice emails [](https://pennylane.readme.io/v1.0/docs/how-to-send-estimates-invoices-by-emails#send-invoice-emails) ------------------------------------------------------------------------------------------------------------------------------ 1. [Create](https://pennylane.readme.io/reference/customer_invoices-post-1) (or [import](https://pennylane.readme.io/reference/customer_invoices-import) ) the invoice 2. Get the external\_id of the invoice in the response ![](https://files.readme.io/f8bda5d-API_response.png "API response.png") 3. Send the email to the client using [this endpoint](https://pennylane.readme.io/reference/customer_invoices-send_by_email-1) : we will ask you the invoice external\_id. Send estimate emails [](https://pennylane.readme.io/v1.0/docs/how-to-send-estimates-invoices-by-emails#send-estimate-emails) -------------------------------------------------------------------------------------------------------------------------------- 1. [Create](https://pennylane.readme.io/reference/customer_estimates-post) the estimate 2. Get the external\_id of the estimate in the response 3. Send the email to the client using [this endpoint](https://pennylane.readme.io/reference/customer_invoices-send_by_email-1) : we will ask you the estimate external\_id. Sending those emails requires that the PDF file for the document has been generated (this process can take a few minutes), so if you just created the invoice / estimate in our system, we may return a 409 error. You should retry the request in a few minutes - if you receive a 204 response, that means that the email is on its way. Email address [](https://pennylane.readme.io/v1.0/docs/how-to-send-estimates-invoices-by-emails#email-address) ------------------------------------------------------------------------------------------------------------------ To send the email you need to specify the email address of the client before. You can do it when creating the estimate / invoice. If the email address of the client is missing, you will receive an error message telling you to first specify the email address. You can add several email addresses to the client (it's an array field), we will send the document to all of them. ![](https://files.readme.io/8671840-email_address.png "email address.png") At the moment, it is not possible to specify another email address in cc or bcc. The emails will be sent with the following address: [\[email protected\].](https://pennylane.readme.io/cdn-cgi/l/email-protection#6701060413121506130e080927170209091e0b0609024904080a49) If your client answers the email, their answer will be automatically transferred to the email address you've specified in the Paramètres > "Facturation clients" in the platform. ![](https://files.readme.io/0ba2f80-sender_email_address.png "sender email address.png") Email templates [](https://pennylane.readme.io/v1.0/docs/how-to-send-estimates-invoices-by-emails#email-templates) ---------------------------------------------------------------------------------------------------------------------- At the moment, it is not possible for you to select or create email templates with the API. We use by default the default invoice and estimate templates you already have in the platform. Depending of the billing language of the customer, we will either select the French default one or the English default one. You can see them in the Parameters > "Facturation clients" > "Emails" . Feel free to modify the content of those templates if needed. Updated 4 months ago * * * Ask AI --- # Filter my customers [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for customers [](https://pennylane.readme.io/v1.0/docs/filter-my-customers#allowed-filter-combinations-for-customers) ----------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | customer\_id | The thirdparty customer id | String | `eq` | | name | The thirdparty name | String | `eq`, `not_eq` | | country\_alpha | The thirdparty country | String | `eq`, `not_eq` | | payment\_conditions | The thirdparty payment conditions | String | `eq`, `not_eq` | Updated 4 months ago * * * Ask AI --- # Filter my invoices [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for invoices [](https://pennylane.readme.io/v1.0/docs/filter-my-invoices#allowed-filter-combinations-for-invoices) -------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | status | The invoice status | Look at the invoice status values below | `eq` | | source | The invoice source | Look at the invoice source values below | `eq`, `not_eq` | | customer\_id | The invoice customer id | String | `eq` | | supplier\_id | The invoice supplier id | String | `eq` | | currency\_amount | The invoice amount in currency | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | currency\_amount\_before\_tax | The invoice amount before tax in currency | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | amount | The invoice amount in euros | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | date | The invoice issue date | String (ISO 8601) | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | deadline | The invoice deadline date | String (ISO 8601) | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | invoice\_number | The invoice number | String | `eq` | | quote\_group\_uuid | The invoice quote group | String | `eq`, `not_eq` | List of invoice status values [](https://pennylane.readme.io/v1.0/docs/filter-my-invoices#list-of-invoice-status-values) ---------------------------------------------------------------------------------------------------------------------------- | Value | Description | | --- | --- | | `incomplete_status` | An imported invoice that does not have all its information completed | | `draft_status` | An invoice that is still in draft mode | | `upcoming_status` | A finalized invoice that is not paid and for which deadline is upcoming | | `late_status` | A finalized invoice that is not paid and for which deadline is passed | | `paid_status` | A paid invoice | | `cancelled_status` | A finalized invoice that is linked with credit\_note and credit\_note covers all the currency\_amount of the original invoice. | | `partially_cancelled_status`: | A finalized invoice that is linked with credit\_note and credit\_note does not cover all the currency\_amount of the original invoice | | `credit_note_status` | A credit\_note (invoice with a negative amount) | List of invoice source values [](https://pennylane.readme.io/v1.0/docs/filter-my-invoices#list-of-invoice-source-values) ---------------------------------------------------------------------------------------------------------------------------- | Value | Description | | --- | --- | | `quotes` | An invoice coming from Pennylane invoice editor | | `stripe` | An invoice coming from Stripe | | `external_api_create` | An invoice created by Pennylane API | | `external_api_import` | An invoice imported by Pennylane API | | `API` | An invoice coming from Pennylane API (created or imported) | | `other` | An invoice coming from another source | Updated 4 months ago * * * Ask AI --- # V1 - Setting up filters [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) A filtering system is available on listing endpoints to let you get resources according to your needs. How does it work ? [](https://pennylane.readme.io/v1.0/docs/how-to-set-up-filters#how-does-it-work-) -------------------------------------------------------------------------------------------------------- You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. A filter object is always a combination of the three following keys : | Name | Description | | --- | --- | | field | The field of the concerned resource you want to filter by | | operator | The operator you want to compare by | | value | The value of the given field you want to filter by | Available operators [](https://pennylane.readme.io/v1.0/docs/how-to-set-up-filters#available-operators) ----------------------------------------------------------------------------------------------------------- | Operator | Description | | --- | --- | | `eq` | equal to | | `not_eq` | not equal to | | `lt` | lesser than | | `lteq` | lesser than or equal to | | `gt` | greater than | | `gteq` | greater than or equal to | Example #1 : Filter the invoices per source (ex: Stripe) [](https://pennylane.readme.io/v1.0/docs/how-to-set-up-filters#example-1--filter-the-invoices-per-source-ex-stripe) -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Let's say you want to retrieve all invoices from Stripe. You need the field source, with operator **eq**, with value set to **stripe**. Ruby `filters = [ { field: 'source', operator: 'eq', value: 'stripe' }, ].to_json` Example #2 : Filter the invoices per client id [](https://pennylane.readme.io/v1.0/docs/how-to-set-up-filters#example-2--filter-the-invoices-per-client-id) --------------------------------------------------------------------------------------------------------------------------------------------------------------- Now, let's say you want to retrieve all pending estimates for the customer whose id is MY\_CUSTOMER\_ID. You need 2 filters. When filtering by customer\_id, the `eq` operator will return the invoices with a `customer_id` similar to the passed value. If you want an exact match, you can use the `match` operator. See the following example with two filters : Ruby `filters = [ { field: 'status', operator: 'eq', value: 'estimate_pending_status', }, { field: 'customer_id', operator: 'match', value: 'MY_CUSTOMER_ID', }, ].to_json` Updated 4 months ago * * * Ask AI --- # Filter my suppliers [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for suppliers [](https://pennylane.readme.io/v1.0/docs/filter-my-customers-copy#allowed-filter-combinations-for-suppliers) ---------------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | supplier\_id | The thirdparty customer id | String | `eq` | Updated 4 months ago * * * Ask AI --- # How to reach out to us [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) If you are a customer in need of support, please reach out to our support teams through the usual process (chat or account manager). If you are a partner in need of support, please use your Zendesk access. If you don't have one, request it to [\[email protected\]](https://pennylane.readme.io/cdn-cgi/l/email-protection#bbcbdac9cfd5dec9c8d3d2cbc8fbcbded5d5c2d7dad5de95d8d4d6) . Updated 4 months ago * * * Ask AI --- # Filter my billing subscriptions [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for billing subscriptions [](https://pennylane.readme.io/v1.0/docs/filter-my-billing-subscriptions#allowed-filter-combinations-for-billing-subscriptions) ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | status | The invoice status | Look at the invoice status values below | `eq` | | customer\_id | The invoice customer id | String | `eq` | | start | The invoice issue date | String (ISO 8601) | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | finish | The invoice deadline date | String (ISO 8601) | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | List of invoice status values [](https://pennylane.readme.io/v1.0/docs/filter-my-billing-subscriptions#list-of-invoice-status-values) ----------------------------------------------------------------------------------------------------------------------------------------- | Value | Description | | --- | --- | | `stopped` | A billing subscription that is paused | | `draft` | A billing subscription that is still in draft mode | | `not_started` | A billing subscription with start\_date in future | | `finished` | A billing subscription that finished all the occurences | | `in_progress` | A billing subscription that is currently running | Updated 4 months ago * * * Ask AI --- # Filter my estimates [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) You need to add a **filter** parameter to your GET request. This parameter has to be a **JSON array of objects**. Please refer to the filters main section to see how to build correctly the **filter** parameter. Allowed filter combinations for estimates [](https://pennylane.readme.io/v1.0/docs/filter-my-estimates#allowed-filter-combinations-for-estimates) ----------------------------------------------------------------------------------------------------------------------------------------------------- | Field | Description | Value(s) | Available operators | | --- | --- | --- | --- | | status | The estimate status | Look at the estimate status values below | `eq` | | source | The estimate source | Look at the estimate source values below | `eq`, `not_eq` | | customer\_id | The estimate customer id | String | `eq` | | currency\_amount | The estimate amount in currency | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | currency\_amount\_before\_tax | The estimate amount before tax in currency | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | amount | The estimate amount in euros | String | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | date | The estimate issue date | String (ISO 8601) | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | | deadline | The estimate expiration date | String (ISO 8601) | `eq`, `not_eq`, `lt`, `gt`, `lteq`, `gteq` | List of estimate status values [](https://pennylane.readme.io/v1.0/docs/filter-my-estimates#list-of-estimate-status-values) ------------------------------------------------------------------------------------------------------------------------------- | Value | Description | | --- | --- | | `estimate_pending_status` | An estimate waiting to be denied or accepted | | `estimate_accepted_status` | An estimate that has been accepted | | `estimate_denied_status` | An estimate that has been denied | List of estimate source values [](https://pennylane.readme.io/v1.0/docs/filter-my-estimates#list-of-estimate-source-values) ------------------------------------------------------------------------------------------------------------------------------- | Value | Description | | --- | --- | | `quotes` | An estimate coming from Pennylane estimate editor | | `external_api_create` | An estimate created by Pennylane API | Updated 4 months ago * * * Ask AI --- # Automating payment matching [These docs are for v1.0. Click to read the latest docs for v2.0.](https://pennylane.readme.io/docs) How does it work? [](https://pennylane.readme.io/v1.0/docs/automatic-payment-matching-1#how-does-it-work) ------------------------------------------------------------------------------------------------------------- If you have a payment to link with the invoice you are creating / importing, it is possible to automatically reconciliate the invoice with this transaction. The only condition is to be able to give us a **unique id** so that we know which transaction to match with the invoice. To do so, you need to use the **transactions\_reference** object. ![](https://files.readme.io/6720b8d-transaction_reference.png "transaction_reference.png") If you don't want to use the automatic matching, please leave all the fields of the transactions\_reference object empty. What are the use cases we currently support? [](https://pennylane.readme.io/v1.0/docs/automatic-payment-matching-1#what-are-the-use-cases-we-currently-support) ------------------------------------------------------------------------------------------------------------------------------------------------------------------- We currently support automatic matching with the invoice number and Stripe payment ID. If you would like to use another unique ID, do not hesitate to contact us: we will assess together if we can add your use case. \# 1: Invoice number (with any bank) [](https://pennylane.readme.io/v1.0/docs/automatic-payment-matching-1#-1-invoice-number-with-any-bank) ----------------------------------------------------------------------------------------------------------------------------------------------- To use the invoice number as a key, you first need to make sure that your client will write the invoice number in the label of the transfer (or in the label of the direct debit). As the invoice number is unique, we will then be able to automatically match the invoice with the bank transaction we'll received on the other hand. In this case, you need to fill the transactions\_reference object as follows: transactions\_reference banking_provider: 'bank' provider_field_name: 'label' provider_field_value: 'invoice_number' This way, we will look into all your bank transactions and detect which transaction has a label which contain the invoice number of the sent invoice. It's not a problem if the label does not only contrains the invoice number. You can also give us any other unique reference that will be in the label. For example: transactions\_reference banking_provider: 'bank' provider_field_name: 'label' provider_field_value: my_unique_reference_1234 \# 2: Stripe [](https://pennylane.readme.io/v1.0/docs/automatic-payment-matching-1#-2-stripe) ------------------------------------------------------------------------------------------------- If you are creating the invoice after the Stripe paiement has been initiated, then you can communicate us the payment\_id of the Stripe transaction. As this payment\_id is unique, we will then be able to automatically match the invoice with the Stripe transaction we'll receive on the other hand. In this case, you need to fill the transactions\_reference object as follows: transactions\_reference banking_provider: 'stripe' provider_field_name: 'payment_id' provider_field_value: enter the Stripe paiement id (something similar to pi_XXXX) You can also fill the transactions\_reference object as follows to do the matching with a charge\_id: Text banking_provider: 'stripe' provider_field_name: 'charge_id' provider_field_value: enter the Stripe charge id (something similar to ch_XXXX) In case you use Stripe aggregated reports, you can, **afterwards** link your invoices to the aggregated transaction by using this reference: Text banking_provider: 'stripe' provider_field_name: 'report_id' provider_field_value: enter the Stripe report id (something similar to frr_XXXX) \# 3: GoCardless [](https://pennylane.readme.io/v1.0/docs/automatic-payment-matching-1#-3-gocardless) --------------------------------------------------------------------------------------------------------- If you are creating the invoice after the GoCardless paiement has been initiated, then you can communicate us the payment\_id of the GoCardless transaction. As this payment\_id is unique, we will then be able to automatically match the invoice with theGocardless transaction we'll receive on the other hand. In this case, you need to fill the transactions\_reference object as follows: Text banking_provider: 'gocardless' provider_field_name: 'payment_id' provider_field_value: enter the Gocardless paiement id (something similar to PM00XXXXXXX) Updated 4 months ago * * * Ask AI --- # API Versioning Pennylane's API is versioned. **The V1 is deprecated as of july 2025.** Our users will have until end of 2025 to migrate to the V2. Read Our version numbers are computed using a two part versioning scheme: {major version}.{minor version}. The major number is directly set in the url : `https://app.pennylane.com/api/external/`. For instance, our first version is available at: `https://app.pennylane.com/api/external/v1` This versioning strategy will enable you to keep track of the smaller updates that happen on Pennylane side (we'll usually only change the minor version), as well as the more significant ones (we'll change the major version number). A major version change will most likely mean an update of the API version on your side. > 🚧 > > ### > > API Depreciation Strategy > > [](https://pennylane.readme.io/docs/api-versioning#api-depreciation-strategy) > > When we decide to deprecate a specific version, we will notify you by email at least **one month** in advance. > > All of our API responses include the header `Api-Version-Expires-At`. If you are on the latest available version, this header should be empty. If you are on an older version, this header will have an expiration date. As soon as the expiration date is reached, the version will no longer be supported. Updated 3 months ago * * * Ask AI ---