maesn
For developers

How to integrate with Sage Accounting: An optional header that decides whose books you write to

Sage Accounting is documented well enough that its two sharp edges are easy to read past. The access token lasts five minutes and every refresh replaces the refresh token, so the correctness of your token store decides whether a connection survives. And the header that says whose books a request touches is optional, so omitting it raises no error, it writes into a different company.

Lennart Svensson, CTO and Co-Founder at Maesn
Lennart Svensson
CTO and Co-Founder · · Updated
Illustration for How to Integrate with Sage Accounting: An Optional Header
The context

One token, one user and several sets of books

An access token here belongs to a user account rather than to a company, and one user can reach several companies. That decides the order of your onboarding, because Sage documents that the number is not knowable at the moment you would most want to know it.

The sentence in its own guide leaves no room: “At present, it is not possible to determine during authentication if the authenticated user has access to more than a single business.” Consent finishes, you hold a token and you still do not know whether there is one set of books behind it or four.

So there is a step between consent and ready. A GET /businesses returns everything that user can reach, each entry with a display name and a 32-character id. Since July 2025 the same call filters on active subscriptions, on name and on product family, and it pages.

What the businesses endpoint answers, in the shape it answers itJSON
GET /businesses
 
{
"$total": 3,
"$items": [
{ "displayed_as": "Business One",
"id": "0d40342d0287cd6b13555c54afca6c90" },
{ "displayed_as": "Business Two",
"id": "49c196153fb1524b7d888cece84e8b12" }
]
}

Three results, and nothing in the authorisation response said so. The id is what the X-Business header carries later.

The same endpoint answers a second question worth asking while a connection is being set up. A GET on a single business returns a subscriptions array, and Sage advises establishing which subscription a customer runs before you rely on anything, because endpoint availability is documented per region and per variant rather than once.

Three tiers arrive under five subscription ids
Subscription idWhat it means
STARTStart
MICROStart, created via Partner Edition
ACCOUNTSAccounting Standard, before May 2020
ACCOUNTING_10Accounting Standard, after May 2020
ACCOUNTINGAccounting Plus, from May 2020

Subscription ids, the availability of endpoints per region and per variant and the advice to check the connection first are documented in Sage’s best practices guide, checked 17 August 2026.

Read that as a feature being absent for reasons that have nothing to do with your code. The same response carries an active flag, and refusing a connection with a sentence your customer understands beats discovering an inactive subscription three endpoints later.

One more instruction in the multi-business guide is stronger than it looks: anything you store locally has to be identified and related by business id, so that data from two businesses cannot merge into a single representation. The id is the partition key, not a label on the row.

Through Maesn the discovery and the selection happen once inside the connect flow, and what your code keeps is an account key. What the connection needs from you and what the object coverage looks like in full sit on the Sage Accounting API page. The rest of this article is about what the calls underneath actually do.

The problem

Five minutes, thirty-one days and a refresh you cannot repeat

Two numbers govern the refresh cycle. The access token is valid for 300 seconds and the refresh token for 31 days, and every refresh returns a new refresh token while retiring the one you sent with it.

An hour of continuous work therefore needs twelve access tokens and eleven refreshes, and each of those eleven is a write to your own storage that has to commit before you use what it returned. A crash in that gap does not cost you a request. It costs you the tenant, because the only way back is a fresh authorisation by the customer.

One consequence follows from the rotation rather than from a sentence in the documentation. Two workers refreshing the same connection at the same moment cannot both win, because the token each of them sends is single use. Refreshing has to be serialised per connection, and that is the bug that reproduces least willingly in a test.

Refresh token, five usesEach use retires the last
Use 1
r1a2
issued
Use 2
b3c4
new
Use 3
d5e6
new
Use 4
f7a8
new
Use 5
g9b0
new
Stored per connection
latest_refresh_tokeng9b0

Only the newest value works. Lose the write that stored it and the connection has no way back except a fresh authorisation by the customer.

300 s
Access token

expires_in, so twelve of them cover an hour of work

31 days
Refresh token

Counted from when it was issued, not from its last use

Five refreshes and five different refresh tokens, each one used once and replaced. The value your store has to be holding is the last one. Numbers from Sage's authentication guide, checked 17 August 2026.

The 31 days work in the other direction. A connection nobody touches for a month is gone, and Sage is explicit that the user has to authorise your app again after that. The quiet tenant is the risk here, not the busy one, which is one reason a scheduled read earns its keep even where little changes.

Two smaller details are worth designing for rather than discovering. Access tokens cannot be revoked at all: they stay valid for their five minutes, and what you revoke is the refresh token. And Sage asks you to reserve up to 2.048 bytes for each token, which is a column width rather than a footnote.

expires_in: 300, refresh_token_expires_in: 2678400, the new refresh token in every renewal response, the 60-second authorisation code, the storage size and the statement that access tokens cannot be revoked are all in Sage’s authentication guide, checked 17 August 2026.

How it works

Whose initials end up on the transaction

A token here is an audit identity as well as an access credential. Sage associates the authenticated user’s initials with the transactions created through their token, so the credential in the header decides who appears to have made the entry.

Sage draws the consequence itself for multi-user integrations: keep tokens for a single user and every transaction your application creates carries the same initials, which makes tracing activity to a person very difficult. Its answer is a token pair per individual user rather than one per customer.

A connection through Maesn is authorised by one person, so the documents written through it carry that person’s initials. If your customer’s bookkeeping expects to see who did what, that is a question for your onboarding rather than for your write path, and it decides how many connections a customer needs.

The permission model shows up in one more place, and the documented handling is unusual. A 403 here usually means the user is not the owner of the business and lacks the role, rather than that a token expired. Sage’s instruction is that a client receiving 403 on every request should discard both tokens and ask for a new authorisation, which is the opposite of a retry.

A permission problem that looks like an authentication problem
Retrying a 403 forever is the wrong answer and so is refreshing the token, because neither gives the user a role they do not have. Telling the two apart is what one error model across systems is for, and the recovery path ends with a message to your customer rather than with a backoff.

The user initials, the multi-user consequence and the 403 instruction are quoted from Sage’s best practices guide, checked 17 August 2026.

How it works

Idempotency is a field in the body, and it covers updates too

Sage supports idempotency, and not in the shape most APIs use. There is no header. You put a 32-character hyphenless GUID as an idempotency_id inside the resource in the request body, you generate it yourself, and for the next seven days the same request returns the first result instead of writing a second record.

It is opt-in twice over. The feature does nothing at all unless the caller passes the field, and it exists on a named list of 35 resources rather than everywhere. Reusing an id on a different resource type is an error of its own, IdempotencyResourceMismatch, so a key is scoped to the kind of thing it created.

The key goes inside the resource, and it is yours to generateJSON
POST /v3.1/contacts
Content-Type: application/json
 
{
"contact": {
"idempotency_id": "c3a8adaa26daf36e02f3c672b69e3323",
"name": "Northwind Ltd",
"contact_type_ids": ["CUSTOMER"]
}
}
 
// same id on another resource type:
// { "$dataCode": "IdempotencyResourceMismatch" }

32 hex characters, no hyphens, generated on your side. Sage honours it for seven days and returns the original result rather than creating a second record.

The part that surprises people is that this covers PUT as well as POST, and Sage’s reason is specific to accounting: modifying an object can trigger further actions. Adding a line to a sales invoice records a transaction, and side effects such as sending an email can fire as well. A retried update is not free here.

Sage also states plainly why the field exists. If an unhandled 500 comes back from a POST, the request may well have succeeded and the transaction may already be in the data. A timeout is not an answer, and without an id the only way to find out is to go and look.

The seven-day window is Sage’s, and it matters for one case in particular: a replay out of a dead-letter queue a fortnight later is a new record by design rather than a duplicate the key will absorb. Whatever sends the write, that window is the boundary to design your retries around.

The field name, the 32-character format, the seven-day window, the resource list, the mismatch error and the reasoning for PUT are documented in Sage’s idempotency guide; the sentence about an unhandled 500 on a POST is from its best practices page. Both checked 17 August 2026.

What you get

Three objects read today, and the two that write are one record in Sage

Four of the 37 objects in the coverage matrix carry an enabled operation on this system. Customers and suppliers are the two that go in and come back changed, accounts are readable and booking proposals can be created. Everything else in the matrix is a request rather than a default.

The four objects with an enabled operation today
ObjectReadCreateUpdate
Customersyesyesyes
Suppliersyesyesyes
Accountsyeson requeston request
Booking proposalson requestyeson request

Counted per object from the generated coverage data, checked 17 August 2026: three objects read, three create, two update and none delete, out of 37 rows. 22 of the remaining objects carry at least one combination marked as available on request.

The working shape here is read and correct. Corrections are posted rather than removed, so a product that needs to withdraw a document carries a supersede concept of its own instead of an expectation of a delete call.

Underneath, those two writable objects are one thing. Sage’s regional guide states that customers and suppliers, separate endpoints in its older regional interface, are handled by a single contacts endpoint, and that every customer and supplier is a contact related to addresses.

The role is not a column on that record, it is a list. contact_type_ids takes an array and each value has to be either CUSTOMER or VENDOR, so one contact can hold both roles at once and the same company can sit on both sides of the ledger under one id.

That is worth knowing before you design the join, because the matrix hands you two objects where the books hold one record. Turning the two shapes into one predictable shape is the job of a shared data model.

One warning that costs an afternoon otherwise: this is not the only Sage product and the others do not share the interface. Sage Active speaks GraphQL, carries a different company header and reaches three countries, with a matrix of its own. Anything you read about tokens or headers for another Sage product is documentation for a different system.

The single contacts endpoint and the relationship to addresses are quoted from Sage’s regional considerations guide; the contact_type_ids array and its two permitted values are from its contact and address guide, where the same POST body appears. Both checked 17 August 2026.

What you get

No events, and a rate limit your customers share

No object in the Sage Accounting matrix carries an enabled webhook, so keeping data current is a read you schedule. How often you can run it is decided less by this system’s patience than by a budget your application spends across every business it is connected to.

Counted per object rather than derived: none of the 37 has an event enabled, 25 are marked as available on request and 12 carry no event support in the matrix at all. That marking describes our matrix rather than Sage’s product, so nothing here is a claim about what Sage does or does not send.

Which leaves the pull half of one event model across systems doing all of the work, and it has one property that suits this system in particular: a scheduled read also keeps the 31-day refresh token moving, so freshness and connectivity are bought with the same job.

The limit on that job is two numbers, both per application rather than per business: 1.296.000 requests a day and a maximum of 150 in flight at any moment. The daily figure is 15 a second sustained and will rarely bother anyone. The concurrency ceiling is the one you meet, and it is shared, so a backfill for one customer competes with every other customer’s sync.

Exceeding either returns 429, and Sage recommends wait-and-repeat handling. Because the budget belongs to the app, the place that enforces it is your scheduler rather than your request layer, and the filter and paging underneath are the same one way to filter and page you already use elsewhere.

On demand is not a delivery date
25 objects here carry an on-demand marking in the webhook column. It means technically feasible and not built yet, it carries no timeline and it is least reliable in exactly this column. Design against what is enabled today and tell us which object would change your product.

The rate limit, the concurrency ceiling and the wait-and-repeat recommendation are quoted from Sage’s best practices guide, checked 17 August 2026; the event counts are read per object from the generated coverage data.

Where our part ends

What Maesn covers, and what stays with you

What we take off the list:

  • The token cycle. Refresh ahead of the five minutes, each rotation stored per connection and a clear signal for the case where a re-authorisation really is needed.
  • The business context. Discovery after consent, the selection inside the connect flow and the X-Business header on every request after that.
  • The write surface. Idempotency on writes to this system, and the same REST calls and the same data model you use for every other system in the catalogue.

What stays with you, and the first one is a product decision rather than a task:

  • Which business a connection means, and what happens when that changes. We can attach the id that was selected. We cannot know that your customer has bought a second company.
  • Whose initials the documents carry. Sage writes the authorising user onto the entry, so attribution is settled at connection time rather than at write time.
  • The schedule and the quiet tenant. A customer nobody syncs for 31 days needs authorising again, and only your product knows how current its data has to be.
FAQ

Frequently asked questions

Do I have to send the X-Business header on every Sage Accounting request?

Sage documents it as optional, and that is the trap rather than a convenience. Without it the request runs against the user's lead business, so nothing errors and the data lands somewhere. Sage's own recommendation is to always send it in production. Through Maesn the header is attached for you.

What is a lead business, and can it change?

It is the first business that user created, and Sage names two ways it can move: that business gets deleted, or a user who was only invited to businesses creates their own. Every response except /businesses and /user carries an X-Business header, so you can compare it against the value you stored.

How long do Sage Accounting tokens last?

The access token expires after 300 seconds and the refresh token after 31 days. Each refresh returns a new refresh token and retires the one you sent with it. The authorisation code in front of both is valid for 60 seconds. Access tokens cannot be revoked, only refresh tokens can.

What happens if nobody uses my integration for a month?

The connection stops. The refresh token expires 31 days after it was issued, and Sage is explicit that users then have to authorise your app again. A scheduled read keeps the credential moving as a side effect, which makes an idle tenant the case worth watching rather than a busy one.

How does idempotency work on the Sage Accounting endpoints?

Through a field rather than a header. You put a client-generated 32-character hyphenless GUID as idempotency_id inside the resource in the body, and for the next 7 days the same request returns the first result instead of creating a second record. It covers PUT as well as POST.

What are the Sage Accounting rate limits?

1.296.000 requests per day and a maximum of 150 concurrent requests, both counted per application rather than per business. Exceeding either returns 429, and Sage recommends wait-and-repeat handling. In practice the concurrency ceiling arrives first, because your customers share it.

Which Sage Accounting data can I read and write through Maesn today?

Accounts, customers and suppliers are readable. Customers and suppliers are also creatable and updatable, and booking proposals can be created. Of the 37 objects in the matrix, 22 carry at least one combination marked as available on request, so the useful question names an object rather than a total.

Build once on the Unified API.

Sage Accounting hands you an optional header and a five-minute token, Fortnox holds one socket open per tenant and Twinfield runs on five percent of its credits until it is certified. Build against one interface and each of those becomes a row in a table rather than a project.