maesn
For developers

How to integrate with Pennylane: Scopes agreed in advance and four weeks of changelog

Pennylane hands out no credentials on a form. A partnerships team reads your integration, agrees a scope list and issues a client ID against it, and that list decides which objects you can ever reach. The interface behind it is ordinary until you notice that three of its failure modes answer with a success code.

Lennart Svensson, CTO and Co-Founder at Maesn
Lennart Svensson
CTO and Co-Founder · · Updated
Illustration for How to Integrate with Pennylane: Scopes and Changelogs
The problem

Your client credentials come from Pennylane's partnerships team

Most accounting platforms let you register an application yourself, click through a consent screen and have working credentials within ten minutes. Pennylane does not. There is no self-service registration for integration partners, and the first step of the build is an email rather than a form.

Pennylane states it plainly in its OAuth walkthrough: “Before using OAuth, register your app with Pennylane by contacting our Partnerships team.” Only after that validation do you receive a client ID and secret.

The same requirement appears in Maesn’s Pennylane integration notes: it is “necessary to establish a direct relation with Pennylane and receive your client credentials from them”. What the connection then looks like object by object is on the Pennylane API page.

This is not a queue you can shorten, so it belongs in the project plan rather than in the sprint. It also changes what your sandbox story looks like, because sandbox access follows the same split.

Who can create a sandbox, and who has to ask
RoleAuthenticationSandbox
CompanyCompany API tokenSelf-service, from Test environment in the account
FirmFirm API tokenNot required, the firm account is used directly
Integration partnerOAuth 2.0Requested from the partnerships team by email

Roles, authentication methods and sandbox routes from Pennylane’s getting started guide, checked 13 August 2026. If you build a product on top of Pennylane you are the third row, which is the one that waits.

Store the secret the first time, there is no second time

Pennylane warns that both halves are shown once: “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.” A lost secret does not mean a reset, it means going back through the partner process that got you the first one.

A Maesn sandbox does exist for Pennylane, alongside the ones for DATEV, Fortnox and Twinfield, and it counts for more here than usual because Pennylane publishes no free trial account of any kind. The documented route to it is “please contact us” rather than a signup form, so it shortens the wait on Pennylane without removing the conversation.

Sandbox availability per system from the Maesn sandbox table, where Pennylane carries no free trial and a Maesn-provided environment, checked 13 August 2026.

The problem

Eleven scopes are agreed before the first call

The scope list is the part of the conversation that outlives it. Pennylane asks you to “supply them the details of your integration including the scopes required”, and what you name there is what your token will carry in production.

The grammar is deliberately coarse. A scope is either resource:readonly, which Pennylane defines as “Read-only access (GET endpoints only)”, or resource:all, which is “Full access (read + write + delete)”. There is no create-only and no update-only. You are choosing per resource, not per verb.

Maesn’s connection is configured against eleven of them. Eight are :all, two are read-only and one carries no suffix at all:

The scopes a Pennylane client needs for this connectionTEXT
customers:all suppliers:all
customer_invoices:all supplier_invoices:all
products:all quotes:all
categories:all file_attachments:all
ledger bank_accounts:readonly
transactions:readonly

Verbatim from the prerequisites in our Pennylane integration notes, checked 13 August 2026. ledger is the one in this list that carries no suffix at all, and Pennylane documents it as read and write over journals, ledger entries and their attachments.

Line those eleven up against what the connection actually exposes and the shape of the integration becomes visible before you write anything. Two objects are supported today, nineteen more are readable on request and sixteen are not exposed here at all.

37 objects, 11 agreed scopes2 live, 19 on request
2
Supported today
Customers and Suppliers, both readable and writable.
19
Readable on request
Invoices, bills, credit notes, journal entries, items, quotes and more.
16
Not exposed here
Payments, payment terms, tax rates, expenses, projects, orders.

What is live is the pair every ledger flow starts from. Customers and suppliers read and write today. Everything that carries an amount is a scope conversation rather than a code change.

Object states for Pennylane, counted from our per-system coverage data on 13 August 2026. Three states: supported by Maesn, on demand, and not supported by Pennylane.
What is live is the pair, not a sample

The two supported objects are Customers and Suppliers, and both read and write. That is the counterparty pair that every payables and receivables flow starts from, which makes it a usable base rather than a token gesture.

Everything carrying an amount, from invoices to journal entries, sits in the on-request band, and on request means a conversation rather than a limitation. How those two objects look once they are normalised across systems is the job of the common data model.

Your scope list was agreed in a conversation, which means your code has no record of what was actually granted on the other side. Pennylane answers that directly: GET /me returns the active scopes for your token, and it is the one endpoint that needs no scope of its own to answer.

The first call worth making: what did we actually get?HTTP
GET /api/external/v2/me HTTP/1.1
Host: app.pennylane.com
Authorization: Bearer <ACCESS_TOKEN>
Accept: application/json

Pennylane documents the response as carrying your company ID, your user information and “the list of active scopes for your token”, and notes that “This endpoint does not require a dedicated scope”. From its scopes reference, checked 13 August 2026. Worth logging on every connection, because a missing scope surfaces as a 403 in production otherwise.

A caveat on that page from the page itself: it dates its own scope list “current as of October 2025” and points at the reference for the authoritative version. Treat the names as a starting point for the partner conversation rather than a settled contract.

How it works

Access tokens last a day, refresh tokens ninety

Once the credentials exist the flow is a standard authorisation code grant against app.pennylane.com. The token exchange is unremarkable, and the number to read out of the response is expires_in.

Exchanging the code, and the lifetime that comes backHTTP
POST /oauth/token HTTP/1.1
Host: app.pennylane.com
 
client_id=...&client_secret=...&code=AUTH_CODE
&redirect_uri=...&grant_type=authorization_code
 
{
"access_token": "abc123...",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "def456..."
}

Request shape and response body from Pennylane’s OAuth 2.0 walkthrough, checked 13 August 2026. expires_in is 86.400 seconds, so 24 hours.

Two durations, from the same page: “Access tokens expire after 24 hours” and “Refresh tokens are valid for 90 days”. A seven day credential lifetime circulates in secondary write-ups of this system and matches neither figure, nor anything else in Pennylane’s documentation.

Rotation itself is ordinary. What is worth reading closely is how immediate Pennylane makes it, because that decides whether your refreshes need to be serialised.

One refresh at a time, or you lose both tokens

In Pennylane’s words: “Every time you use a refresh token, it is immediately invalidated ... Once a refresh token is used, both that token and the old access token will no longer work.” The instruction that follows is the one to design around: “ensure your application caches tokens properly and does not attempt concurrent refresh requests using the same token”.

Two workers noticing an expiry in the same second is enough. One wins, the other invalidates the winner’s pair, and the customer is back at the consent screen.

Centralising refresh behind a lock is the whole fix, and it is a great deal cheaper before launch than after. This is the class of problem unified authentication exists to absorb.

There is a second consequence that only shows up in operations. A refresh token survives 90 days of inactivity, so an integration that goes quiet keeps its credentials far longer than the 24 hour access token suggests. That becomes a problem in combination with the changelog window, which the sync section below works through.

Multi-entity products have one more decision here. Pennylane issues tokens in two contexts: a company token reaches a single company, while a firm token “can access resources across multiple client companies managed by the firm”.

The visible set “depends on who granted access and their internal permissions”. For a product selling to accounting practices that distinction is the difference between one connection and hundreds.

How it works

Every object needs its own call, and its own scope

Creating a business document in v2 means one call per object, and Pennylane ties that directly back to the scope list you agreed at the start: “each object has to be created separately, through a dedicated endpoint. This also is enforced by the granularity of scopes.”

Pennylane gives the example that matters for billing. A customer cannot be created inline with an invoice: it is “mandatory to first create the customer” and then pass its ID. File attachments work the same way: upload to /file_attachments, take the ID, then reference it as file_attachment_id on the import.

So the number of round trips per business document is a function of how many objects it touches, and each of those objects needs its own scope. That is the same sentence as the scope section, arrived at from the write side.

Three v2 rules that turn into support tickets

Amounts are strings. Pennylane made it “mandatory to pass them on as strings” to avoid float errors, and sending 120.00 as a number is listed as a cause of 400.

Ledger entries must balance. “All ledger entries must be balanced: debit total = credit total”, and a mismatch comes back as 422 with both totals in the details object.

Nested collections are links. Invoice lines arrive as “a link towards the collection” rather than an inline array, which is why lines are their own row in any coverage table and their own read in your code.

All three from Pennylane’s v1 against v2 guide and its error handling reference, checked 13 August 2026. The same v2 page still says v1 “will be completely depreciated by end of 2025”, which was ten months before this was written, so confirm the current state with Pennylane rather than with the sentence.

Two things v2 will not do for you at all, per Pennylane’s supported use cases: create a downpayment invoice or a delivery note. If either is in your product flow, find out now rather than in the second sprint.

What you get

Filters, cursors and duplicates: Three quiet ways to lose data

Pennylane’s error handling is genuinely good, with a consistent JSON schema, a useful details object and a documented retry matrix. The expensive cases are the ones that never reach it, because they answer with a success code.

The first is pagination. Version two moved to cursors, and the cursor carries less than it looks like it does:

A cursor remembers the position, not the filter

“The cursor only encodes position in the result set, it does not store filter state ... Omitting the filters on page 2+ will return unfiltered results from the cursor position.”

Page one is filtered, page two is not, and both are 200. A draft-invoice sync that looks correct in testing against twenty records starts pulling every invoice in the company the moment a customer crosses one page. Re-send filter and sort on every request, which is also how unified pagination and filtering normalises this across systems.

The second is the changelog checkpoint, and Pennylane flags it as an explicit warning rather than a footnote: “Do not update your start_date checkpoint until you have fully paginated through all pages (has_more: false). If you stop mid-pagination, resume from your last next_cursor.” Advance the checkpoint after page one of four and pages two to four are gone, with nothing anywhere reporting a gap.

The third is duplication on write. Pennylane is direct about it: “If the same request is sent twice, you may create duplicates”, and idempotency is “not enforced on creation endpoints” such as /customer_invoices and /ledger_entries. There is exactly one exception, and it is oddly specific.

Three documented ways to end up with wrong data and a green response
What happensWhat you get backThe fix
Filter dropped on page two200, with unfiltered rows from the cursor positionRe-send filter and sort with every cursor
Checkpoint moved too early200, and the unread pages are never offered againAdvance start_date only on has_more: false
Create request sent twice201 twice, and two records in the ledgerDeduplicate before the call, no idempotency key exists

Pagination behaviour from the cursor pagination guide, the checkpoint warning from the changelog guide and the idempotency statement from the error reference, all checked 13 August 2026.

The one place Pennylane does deduplicate for you

Supplier invoice imports are the exception: “idempotency is not enforced except for supplier invoice imports”. Re-import a PDF that is already present and you get a 409 naming the existing document rather than a second copy, so the effective idempotency key is the file_attachment_id. Useful, and narrow enough that it is worth writing down where your team will find it.

What is left is the ordinary part, and it is well built. A 403 means the token lacks the scope, with the missing name in the message. A 422 means a business rule failed, with the offending field in details.

Pennylane also publishes which codes deserve a retry, 429 and 500 and 503, and which never do, 400 and 401 and 403 and 404 and 422. Mapping those onto one behaviour across systems is what unified error handling is for.

What you get

Webhooks are in beta, and the changelog keeps four weeks

Pennylane does send events. It sends three, they are in beta, and Pennylane’s own advice is not to depend on them yet: “The webhooks feature is in Beta. We recommend falling back on changelog endpoints during this phase.”

The three are customer_invoice.e_invoicing_status_updated, dms_file.created and supplier_invoice.e_invoicing_received. All three are about e-invoicing status or a document arriving, which is a very French set of concerns and a reasonable place to start given the country’s invoicing regime.

Note what is not in that list. Neither of the two objects this connection supports today, customers and suppliers, announces its own changes. For those you poll, which is what the changelog endpoints are for and what unified webhooks has to work around when a system offers no event for an object.

Beta status from Pennylane’s webhooks overview and the three events from its event list, checked 13 August 2026. Both pages changed within the last three weeks, so a beta claim here ages faster than the rest of this article.

The changelog endpoints cover seven resources, including both live objects: customer_invoices, supplier_invoices, customers, suppliers, products, ledger_entry_lines and transactions. Each change gives you an ID and an operation, so the pattern is a delta read followed by a batch fetch.

Polling changes, then fetching the records in one batchHTTP
GET /api/external/v2/changelogs/customers
?start_date=2026-08-06T00:00:00Z&limit=1000
 
{ "items": [ { "id": 123, "operation": "insert" } ],
"has_more": false, "next_cursor": null }
 
GET /api/external/v2/customers?filter=
[{"field":"id","operator":"in","value":[123,456]}]

Endpoints, the in operator and the insert, update, delete operations from the changelog guide, checked 13 August 2026. start_date and cursor are mutually exclusive, so a resumed run uses the cursor and a fresh run uses the date.

Then the limit that decides your architecture: “Changes are retained for 4 weeks”, and you “cannot request changes older than 4 weeks”. That number matters next to the 90 day refresh token because the two expire at different speeds. Credentials survive 90 days of silence. An incremental catch-up stops being possible after 28. So an integration can pause for five weeks, reconnect without an error anywhere, and still be missing a fortnight of changes it can no longer ask for.

How long each thing lastsAxis: 90 days
Access token
24 hours
Refreshed on schedule, so this one never limits you.
Changelog retention
4 weeks
The oldest change you can still ask for.
Refresh token
90 days
Still valid long after the changelog has forgotten.

Day 29 to day 90 is the expensive stretch. A pause that long leaves the credentials working and the incremental sync beyond rescue, and nothing in either response tells you that you crossed the line.

The three durations Pennylane publishes, on one axis. Access token and refresh token from the OAuth walkthrough, retention from the changelog guide, both checked 13 August 2026.
Credentials outlive the changelog by 62 days

An integration paused for five weeks reconnects without a murmur, because the refresh token is good for 90 days. Its changelog cursor, however, is now outside the retention window, and the only way back to a correct local copy is a full re-read of every affected resource.

Nothing in either response announces which side of day 28 you are on. A recorded last successful sync timestamp, plus a rule for what to do past 28 days, is a few hours of work that pays for itself the first time an outage runs long.

Rate limits set the pace of that recovery, and the documented figure is a window rather than a per-second rate. Pennylane allows “up to 25 requests every 5 seconds”, applied “at the token level”, on sandbox as well as production, and with “all endpoints ... affected”. Per token matters: the budget scales with connected companies rather than being shared across your whole app.

What that costs on a recovery run follows from those two figures alone. Reading 10.000 changes at the default page size of 20 takes 500 changelog calls plus 500 batch reads, so 1.000 requests, which is 40 windows and about 200 seconds.

The same recovery at the documented maximum of 1.000 per page is 20 requests and fits inside a single window. The default is the expensive choice, and it is the one you get by not passing limit.

One helpful detail while you tune that: the ratelimit-limit, ratelimit-remaining and ratelimit-reset headers are returned on successful responses too, not only on a 429. You can pace against your remaining budget rather than discovering the ceiling by hitting it.

Limit, scope of application and header behaviour from Pennylane’s rate limiting page, checked 13 August 2026. Page sizes from the changelog guide. The 200 seconds is arithmetic on those two figures rather than a measured benchmark.

Key facts

What Maesn covers, and what stays with you

A unified interface takes over most of what the sections above describe. It does not take over the partner relationship, and that split is what decides a Pennylane project’s timeline, because the half that stays with you has the longest lead time in it.

What the connection takes off your desk:

  • Token lifecycle, including the rotation trap. The 24 hour expiry, the single-use refresh token and the serialisation that stops two workers destroying each other’s credentials.
  • One shape for customers and suppliers. The two supported objects arrive in the same normalised form they take for every other connected system, so the mapping is written once.
  • Pacing against the token budget. Requests are queued and retried against the 25 per 5 seconds ceiling rather than against a 429 you handle yourself.
  • Cursor handling with the filter attached. The failure mode from the reading section stops being something each integration has to remember.
  • A sandbox for a system with no free trial. Pennylane offers no trial account, and a Maesn environment is available on request.

What stays with you, and the first item is a commercial task rather than a technical one:

  • The relationship with Pennylane. The client ID and secret are issued to you, by their partnerships team, against your scope list. Nobody can hold that conversation on your behalf, and it is the long lead time in the plan.
  • Deciding the scope list. Which objects your product needs is a product question. It is also close to irreversible, since widening the list later means going back to the same team.
  • Your own re-sync policy past 28 days. The changelog window is Pennylane’s, and what your product does when it has been exceeded is yours.
  • Deduplication on write. With no idempotency key outside supplier invoice imports, not sending the same create twice remains your responsibility.
Two objects today, and what on demand actually means

Two supported objects reads like a thin connection, and it is not the whole picture. Nineteen further objects are marked “On demand - contact us” rather than unavailable, and the two that are live are the pair that payables and receivables both build on.

So the planning question is not whether Pennylane is covered. It is which of those nineteen your roadmap needs, and when you intend to ask for them.

Object states counted from our per-system coverage data on 13 August 2026. The three state labels are quoted from the legend on our Pennylane integration notes. Supported use cases and their exclusions from Pennylane’s use case overview.

FAQ

Frequently asked questions

How do I get Pennylane client credentials?

Not from a signup form. You contact Pennylane's partnerships team, describe the integration and name the scopes you need, and they issue a client ID and secret once that is validated. Store both immediately: Pennylane states they cannot be retrieved later, and a loss means a new app.

Where can I find Pennylane's partner list?

On Pennylane's technology partner page, which our integration notes link directly. It is the same route as the credentials: registration for an OAuth client runs through the partnerships team, so the commercial conversation and the technical setup start in the same place rather than one after the other.

How long do Pennylane tokens last?

Access tokens expire after 24 hours and refresh tokens are valid for 90 days. Pennylane uses refresh token rotation, so every refresh invalidates the token you just used and returns a new one. Two workers refreshing the same token concurrently will break each other.

What are the Pennylane rate limits?

Twenty-five requests every five seconds, applied per token rather than per app, on production and sandbox alike. Every response carries ratelimit-limit, ratelimit-remaining and ratelimit-reset, including the ones that succeed, so you can pace against the headers instead of against a 429.

Does Pennylane support webhooks?

Three, and they are in beta. Pennylane's own recommendation during the beta is to fall back on its changelog endpoints. All three cover e-invoicing status and document arrival, so neither customers nor suppliers announce their own changes and both are polled.

How far back can I re-sync from the Pennylane changelog?

Four weeks. Changes are retained for that window and the endpoint refuses a start date older than it, so a gap longer than 28 days cannot be closed incrementally. The recovery is a full re-read of the resource, which is a different cost to plan for.

Why does my Pennylane call return 403?

The token is valid but lacks the scope for that resource. Because scopes are agreed with a human team before you build, the useful move is to ask the API what you were actually granted: GET /me returns the active scopes for your token and needs no scope of its own.

Build once on the Unified API.

Pennylane's partner process, its rotating refresh tokens and its four-week changelog are each a week of somebody's time. Build against one interface and each of them becomes a row in a table rather than a project.