2026 API changes Migration Guide
This full guide is here to help you implement all necessary changes that are coming to the Pennylane API in 2026, including ledger scope changes, pagination updates, and other improvements across multiple endpoints.
The Pennylane API is introducing important changes. This guide explains what's changing and how to migrate your integration smoothly using our phased rollout approach.
⚠️You will need to have finished the migration by June 29th 2026 at the latest. You have in total 24 weeks to complete the migration.
📋 What's Changing
Scope Deprecation
The ledger scope is being deprecated and replaced with more granular scopes:
| Old Scope | New Scopes |
|---|---|
ledger | journals:readonly / journals:all |
ledger_accounts:readonly / ledger_accounts:all | |
ledger_entries:readonly / ledger_entries:all |
Note:
The old
ledgerscope will only work on the old behavior system. As soon as you opt-in to the new version, or when the sunset phase start and you didn't explicitely opt-out to the old behavior,ledgerscope will no longer work.Important: We have added automatically all new scopes to the OAuth apps, Oauth Access Grant, and Oauth Access Token that were using the ledger scope before 12th of January 2026.
New Pagination
Some endpoints are using the old page pagination system. We're aligning this scope pagination on the cursor pagination that is used across the rest of V2 endpoints.
Read our complete Cursor Pagination guide here.
Affected endpoints:
- List journals
- List Ledger Accounts
- List Ledger Entries
- List ledger entry lines of a Ledger Entry
- List ledger entry lines lettered to a given ledger entry line
- List categories of a Ledger Entry line (
pageandper_pageparameters are no longer required) - Get the trial balance (
pageandper_pageparameters are no longer required) - List Company's Fiscal Years (
pageandper_pageparameters are no longer required)
Before:
Following offset pagination params are set :
total_pagescurrent_pageper_pagetotal_items
New cursor based pagination attributes are exposed in the response, but set with null value :
has_morenext_cursor
Example:
{
"total_pages": 2,
"current_page": 1,
"per_page": 10,
"total_items": 11,
"items": [
// ...
],
"has_more": null,
"next_cursor": null
}After:
Following offset pagination params are exposed, but set with null value :
total_pagescurrent_pageper_pagetotal_items
New cursor based pagination attributes are working:
has_morenext_cursorvalue is set (or isnullif you reach the end of the list).
Read our complete Cursor Pagination guide here for more details.
Example:
{
"total_pages": null,
"current_page": null,
"per_page": null,
"total_items": null,
"items": [
// ...
],
"has_more": true,
"next_cursor": "eyJpZCI6MTAwfQ=="
}Ledger entries id are not suffixed anymore
The ledger entries ids are currently suffixed with the 3 last digits. This will change in the upcoming version.
Before:
A supplier invoice id, for example, will be returned as 12345, instead of 12345003 with suffix.
Affected endpoints:
- Ledger Entries
- List Ledger Entries (
idin the response) - Create a ledger entry (
idin the response) - Update a ledger entry (
idin the response, andidin the requested params) - Retrieve a Ledger entry (
idin the response) - List ledger entry lines of a Ledger Entry (
ledger_entry_idin the requested params)
- List Ledger Entries (
- Ledger Entry Lines
- List ledger entry lines (
{ ledger_entry: id }in the response) - Retrieve a Ledger entry line (
{ ledger_entry: id }in the response) - List ledger entry lines lettered to a given ledger entry line (
{ ledger_entry: id }in the response)
- List ledger entry lines (
- In the responses of Customer Invoices:
- List customer invoices
- Retrieve a customer invoice
- Create a customer invoice
- Import an invoice with file attached
- Link a credit note to a customer invoice
- Create a customer invoice from a quote
- Update a customer invoice
- Update an Imported customer invoice
- Turn the draft invoice into a finalized invoice
- In the responses of Supplier Invoices:
Filtering and Sorting Restrictions
Breaking Change: Filtering and sorting by created_at and updated_at timestamps are being removed from ledger-related endpoints.
Affected Endpoint)
The following endpoints will no longer support created_at or updated_at filtering/sorting:
What's Being Removed
Old behaviour (no longer supported):
# Filtering by created_at - ❌ NO LONGER WORKS
GET /api/external/v2/ledger_entries?filter=[{"field": "created_at", "operator": "gteq", "value": "2025-04-08"}]
# Filtering by updated_at - ❌ NO LONGER WORKS
GET /api/external/v2/ledger_entries?filter=[{"field": "updated_at", "operator": "lteq", "value": "2025-04-08"}]
# Sorting by created_at - ❌ NO LONGER WORKS
GET /api/external/v2/ledger_entries?sort=created_at
# Sorting by updated_at - ❌ NO LONGER WORKS
GET /api/external/v2/ledger_entries?sort=-updated_at
Migration Strategies
Option 1: Use alternative filters
Instead of created_at, use NEW filter/sort on id (based on unsuffixed ID) or business-relevant date fields :
# Use ID-based sorting (most recent = highest ID)
GET /api/external/v2/ledger_entries?sort=-id
# For ledger entries, use the date field
GET /api/external/v2/ledger_entries?page=1&per_page=20&filter=[{"field": "date", "operator": "gteq", "value": "2024-01-01"}, {"field": "date", "operator": "lteq", "value": "2024-12-31"}]
Option 2: Client-side filtering
If you need to track changes:
# Fetch all records and filter locally
GET /api/external/v2/ledger_entries
# In your application:
# - Cache the last sync timestamp
# - Store the highest ID you've seen
# - On next sync, fetch records with id > last_seen_id
Option 3: Changelogs (recommended for change tracking)
For real-time updates, consider using changelogs instead of polling with timestamp filters:
Default Sort Order
The default sort order is changing from ascending to descending on the following endpoints:
For List Company's Fiscal Years endpoint, the default order is changing from +start to -id.
id,created_atandupdated_atattributes has been added to the response.- New sort query param has been added allowing you to sort by
idorstart.
Migration:
# To maintain current ascending order
GET /api/external/v2/journals?sort=+id
# To use new descending order
GET /api/external/v2/journals?sort=-id
# List Fiscal years :
# To maintain current start ascending order
GET /api/external/v2/fiscal_years?sort=+start
# To use new descending order
GET /api/external/v2/fiscal_years?sort=-idDefault Filtering Removed
The ledger_entries#index endpoint will no longer filter :
- by open fiscal years** by default.
- draft entries
What this means:
- All ledger entries (including the one in draft state) will be returned (not just from open current fiscal year)
- The
datefield can now benull - You can still filter explicitly using the
dateparameter
Migration:
# If you need to maintain filtering by fiscal year
GET /api/external/v2/ledger_entries?filter=[{"field":"date","operator":"gte","value":"2024-01-01"}]Response Structure Changes
We're improving our API responses by exposing full objects instead of simple ID references:
Journal Object
Before:
{
"journal_id": 42
}After:
{
"journal": {
"id": 42,
"url": "https://app.pennylane.com/api/external/v2/journals/42"
}
}Affected endpoints:
Ledger Account Object
Before:
{
"ledger_account_id": 42
}After:
{
"ledger_account": {
"id": 42,
"number": "401000",
"url": "https://app.pennylane.com/api/external/v2/ledger_accounts/42"
}
}Affected endpoints:
Attachment Object
In the response, ledger_attachment_id is replaced with attachment
Before:
{
"ledger_attachment_id": 42,
"ledger_attachment_filename": "receipt.pdf"
}After:
{
"attachment": {
"filename": "receipt.pdf",
"url": "https://..."
}
}Request parameter change:
- Old:
ledger_attachment_id - New:
file_attachment_id
Affected endpoints:
- Create a ledger entry
- Update a ledger entry
- List Ledger Entries (deprecating
ledger_attachment_filenameis present and deprecated)
Endpoint Deprecation
Deprecated: POST /api/external/v2/ledger_attachments (Upload a file)
Use instead: POST /api/external/v2/file_attachments (Upload a file)
Important: You'll need the
file_attachments:allscope to use the new endpoint.
Error Message Improvements
Error messages for 404 responses have been modified:
Affected endpoints:
# NOW
{
"status": 404,
"error": "Couldn't find Journal with 'id'=123"
}# NOW
{
"status": 404,
"error": "Couldn't find LedgerAccount with 'id'=123"
}# NOW
{
"status": 404,
"error": "Couldn't find LedgerEntry with 'id'=123"
}# NOW
{
"status": 404,
"error": "Couldn't find LedgerEntryLine with 'id'=123"
}- Retrieve a company customer
- Update a company customer
- Retrieve an individual customer
- Update an individual customer
- Retrieve a customer
- Update a billing subscription
- Create a SEPA mandate
- Update a SEPA mandate
- Associate a GoCardless mandate to a customer
- Send a GoCardless mandate email request
# NOW
{
"status": 404,
"error": "Couldn't find Customer with 'id'=123"
}# NOW
{
"status": 404,
"error": "Couldn't find Supplier with 'id'=123"
}🚀 Migration Strategy
We're using a three-phase rollout to ensure a smooth transition:
Phase 1: Preview Phase (12 weeks)
Timeline: Starting 12th of January 2026
Default Behavior: Old behavior
Action Required: Opt-in to test and migrate to the new behavior
During this phase, the new behavior is opt-in only. This is your opportunity to test the changes without any risk.
How to opt-in:
Using headers (recommended for production):
curl -H "X-Use-2026-API-Changes: true" \
-H "Authorization: Bearer YOUR_TOKEN" \
https://api.pennylane.com/api/external/v2/ledger_entriesUsing query parameters (easier for testing):
curl "https://api.pennylane.com/api/external/v2/ledger_entries?use_2026_api_changes=true" \
-H "Authorization: Bearer YOUR_TOKEN"Phase 2: Sunset Phase (12 weeks)
Default Behavior: New behavior ⚠️ BREAKING CHANGE
Action Required: Opt-out if you need more time to migrate
During this phase, the new behavior becomes the default. If you're not ready, you can temporarily opt-out.
How to opt-out:
Using headers (recommended for production):
curl -H "X-Use-2026-API-Changes: false" \
-H "Authorization: Bearer YOUR_TOKEN" \
https://api.pennylane.com/api/external/v2/ledger_entriesUsing query parameters (easier for testing):
curl "https://api.pennylane.com/api/external/v2/ledger_entries?use_2026_api_changes=false" \
-H "Authorization: Bearer YOUR_TOKEN"Warning: This is a temporary safety valve. You must complete your migration before the Cleanup phase.
⚠️ If both header and query parameter are provided with conflicting values, you'll receive a 400 Bad Request error. Please use one or the other.
Phase 3: Cleanup Phase (Permanent)
Timeline: At the end of the sunset phase, on June 29th 2026. Default Behavior: New behavior only
During this phase:
- All compatibility code is removed
- The opt-in/opt-out mechanism is disabled
- Only the new behavior exists
You must have migrated by this point.
📊 Behavior by Phase
| Phase \ Header value | No Header | true | false |
|---|---|---|---|
| Preview | OLD behavior | New behavior | OLD behavior |
| Sunset | ⚠️ New behavior | New behavior | OLD behavior |
| Cleanup | New behavior | New behavior | New behavior |
🎯 Recommended Migration Paths
Option A: Early Adoption (Recommended)
- Week 1-2: Set
X-Use-2026-API-Changes: truein your testing environment - Week 2-8: Test thoroughly with new behavior
- Week 8+: Deploy to production with header
X-Use-2026-API-Changesset totrue - Week 12: No action needed when Sunset phase starts ✅
Benefits:
- Zero disruption during Sunset phase
- More time for thorough testing
- No emergency fixes needed
Option B: Proactive Opt-Out
- Before Week 12: Set
X-Use-2026-API-Changes: falsein production - Week 12: Sunset phase starts (no disruption, header keeps old behavior)
- Week 12+: Complete migration in your testing environment removing the
X-Use-2026-API-Changesheader - Before Week 24: Deploy to production and remove header ✅
Benefits:
- Extra time to prepare
Option C: Reactive Opt-Out (Emergency Only)
- Week 12: Discover breaking change when Sunset starts
- Week 12 (Day 1): Immediately set
X-Use-2026-API-Changes: false - Week 12+: Rush to complete migration in your testing environment removing the
X-Use-2026-API-Changesheader. - Week 24: Deploy to production and remove header ✅
Warning: High-stress migration path. Not recommended.
🧪 Testing Your Integration
Testing new behavior during Preview Phase
# Test new behavior
curl -H "X-Use-2026-API-Changes: true" \
-H "Authorization: Bearer YOUR_TOKEN" \
https://api.pennylane.com/api/external/v2/ledger_entries
# Verify that new behavior is available:
# - Test the new Cursor base pagination system
# - Ledger Entries ID are not suffixed anymore with 3 digits ("00X")
# - Not found "new" error message
# - Check default sort order
# - Test filter on Ledger Entry ID (created_at and updated_at renders an error)
# - Test that Ledger Entries outside of open fiscal period are rendered by defaultTesting Old behavior during Sunset Phase
# Test that you can revert if needed
curl -H "X-Use-2026-API-Changes: false" \
-H "Authorization: Bearer YOUR_TOKEN" \
https://api.pennylane.com/api/external/v2/ledger_entries
# Verify old behavior is restored
# - Deprecated Offset Pagination system
# - Ledger Entries ID are suffixed with 3 digits ("00X")
# - Not found "old" error message
# - Check default sort order
# - Filter on Ledger Entry ID is not allowed, created_at and updated_at works
# - Test that Ledger Entries outside of open fiscal period are NOT rendered by default❓ FAQ
When will these changes take effect?
The Preview phase begins on 12th of January 2026. You'll receive an email notification before it starts.
The new behavior is adopted across all endpoints by default on the 6th of April 2026.
**By 29th of June 2026, no rollback is possible. ** You have in total 24 weeks to complete the migration.
Do I need to update my OAuth scopes immediately?
The old ledger scope will only work on the old behavior system. As soon as you opt-in to the new version, or when the sunset phase start and you didn't explicitely opt-out to the old behavior, ledger scope will no longer work.
Important: We have added automatically all new scopes to the OAuth apps, Oauth Access Grant, and Oauth Access Token that were using the ledger scope before 12th of January 2026.
I have a developer token, what do I need to do?
If you already have a developer token generated before 12th of January 2026 with the ledger scope, you have nothing to do. If you need to generate a new token, make sure you select the new scopes.
What happens if I don't set any header?
- Preview phase: You'll get the old behavior (safe)
- Sunset phase: You'll get the new behavior (⚠️ breaking change)
- Cleanup phase: You'll get the new behavior (only option)
Can I use both headers and query parameters?
Yes, but they must have the same value. If they conflict, you'll receive a 400 Bad Request error.
How long do I have to migrate?
You have approximately 24 weeks from the start of the Preview phase until the Cleanup phase begins.
What if I need help?
Contact our support team !
⚠️We will not do any support through readme discussions space.
Will this affect the Firm API?
No, this is only about the V2 API.
📅 Timeline Summary
┌───────────────────────────────────────────────────────────┐
│ │
│ Week 1 Week 12 Week 24 │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Preview Phase Sunset Phase Cleanup Phase │
│ (12 weeks) (12 weeks) (Permanent) │
│ │
│ Default: Old Behavior New Behavior New Behavior │
│ Action: Opt-in Opt-out N/A │
│ │
└───────────────────────────────────────────────────────────┘
📝 Migration Checklist
Use this checklist to track your migration progress:
- Review all affected endpoints in your integration
- Update OAuth scopes from
ledgerto granular scopes, or generate a new developer token if necessary - Test with
X-Use-2026-API-Changes: truein staging - Update code to use new object structures (
journal,ledger_account,attachment) - Update code to use
file_attachment_idinstead ofledger_attachment_id - Rely on
ledger_entryunsuffixed ID in response and request. - Rely on the new cursor base pagination system
- Add explicit
sortparameter if you rely on ascending order - Add explicit
datefiltering if you rely on fiscal year filtering - Update error handling for new 404 error messages
- Test all ledger-related API calls
- Deploy to production with header set
- Monitor for errors after Sunset phase begins
- Remove header after Cleanup phase (optional, but recommended)
Need Help?
If you have questions or need assistance with your migration, contact our support team for help.
⚠️ No support will be done from the Discussions space, as it's not efficient for personalized assistance.
We're here to help ensure a smooth transition! 🚀
Updated 1 day ago
