How to integrate with FreeAgent: The access level decides which fields come back
FreeAgent is a small surface to learn and a quiet one to get wrong. What a read gives back is not settled by the endpoint you call: the grade of the user who connected decides which fields arrive, the view you did not name decides which rows do, and whether you can ask for changes at all depends on when that endpoint got its filter. All three answer 200.


One contact record, and two roles that read from it
FreeAgent keeps one party record. Customer and supplier are not two objects with two endpoints, they are two named views over /v2/contacts, and that single design decision explains a role split which otherwise looks like a data error.
What creates a role is activity, not a field you set. A contact starts as neither a customer nor a supplier, and it becomes one the first time an invoice ties the two of you together: one you raise makes it a customer, one you receive makes it a supplier. A contact that has done neither stays unused, so an account holds three groups rather than two. The role is a reading of history, not a property of the record.
That is what the coverage of this system reflects. Customers and suppliers are readable through Maesn today and contacts is writable, because there is one record to write and no way to create it as a role. Read the roles apart, write the record underneath. Which objects a connection reaches is on the FreeAgent API page.
Most descriptions of this system stop there. Two readable roles and one writable record is a narrow surface, so what decides your build is how complete those two reads are, and FreeAgent narrows them in three documented ways that all return a 200.
Access levels run 0 to 8, and they reach the fields
FreeAgent does not use named scopes. It grades user permissions on a numbered scale and states a minimum per resource: “Access to FreeAgent resources is limited by user permissions. The minimum required access level is specified for each resource.”
The scale runs 0 : No Access, 1 : Time, 2 : My Money, 3 : Contacts & Projects, 4 : Invoices, Estimates & Files, 5 : Bills, 6 : Banking, 7 : Tax, Accounting & Users and 8 : Full.
The grade is a property of the person who authorised the connection rather than of your application, so a restricted user is a restricted connection. It does not stop at the door either: the same grade decides how much of a record you get once you are through it.
The contacts resource states a minimum of Time, level 1 of 8. Halfway down its attribute table FreeAgent puts a divider line reading “Additional attributes available to users with Contacts & Projects permission”.
Above that line sit 8 attributes. Below it sit 22, and they are the ones an integration usually wants: email, the address, the payment terms and the tax registration number. Same endpoint, same record, same 200 status.
- url
- first_name
- last_name
- organisation_name
- active_projects_count
- direct_debit_mandate_state
- created_at
- updated_at
The minimum the resource states. A name, two timestamps and a URL.
- billing_email
- phone_number
- mobile
- address1
- address2
- address3
- town
- region
- postcode
- country
- uses_contact_invoice_sequence
- contact_name_on_invoices
- charge_sales_tax
- sales_tax_registration_number
- status
- default_payment_terms_in_days
- locale
- is_cis_subcontractor
- cis_deduction_rate
- unique_tax_reference
- subcontractor_verification_number
Everything a billing address, a payment term or a tax number needs. The four dashed ones have a second condition that has nothing to do with permissions: the company has to have CIS for Contractors switched on.
This is why the same integration returns rich contacts for one customer and bare names for another, with nothing in either response to say why. A field that is absent because of a permission looks exactly like a field the customer never filled in, and both arrive as no key at all rather than as an explicit null.
Four fields carry a second condition that has nothing to do with permissions. The CIS attributes is documented as one that “will not be present if CIS for Contractors is not enabled for the company”, so a company setting removes them independently of who connected. Two reasons and one symptom. Treating either as a bug in your mapper sends you looking in the wrong place.
Field sets that differ per tenant are the general case of this, and customisation handling is where that lives as a platform question rather than a FreeAgent one. The practical rule for this system is narrower: treat every attribute below the divider as optional in your schema, and record the grade you were given at connection time so the gap has an explanation later.
The scale, the sentence about user permissions and the per-resource minimum from FreeAgent’s introduction; the divider line, the two attribute groups and the CIS condition from its contacts reference, both checked 17 August 2026.
The default view hides rows, and the two role filters are not symmetrical
The second narrowing is in the list call. /v2/contacts takes a view parameter with nine values, and leaving it out is not the same as asking for everything. The documented default is “active”: Show only active contacts.
A contact carries a status of Active or Hidden, and hiding one is how a FreeAgent user tidies a list without deleting history. Those records stay in the ledger, stay attached to their invoices and are simply absent from the response you get by default.
| View | What it returns |
|---|---|
| all | Every contact |
| active | Only active contacts. This is the default |
| clients | All clients |
| suppliers | Only active suppliers |
| active_projects | Clients with active projects |
| completed_projects | Clients with completed invoices |
| open_clients | Clients with open invoices |
| open_suppliers | Suppliers with open bills |
| hidden | Only hidden contacts |
Read the third and fourth rows next to each other, because they are the ones that matter for the party layer. clients returns all clients. suppliers returns active suppliers only. The two roles you read apart are not equally complete, and nothing in either response says so.
So a customer sync that walks clients and a supplier sync that walks suppliers are not the same operation with a different filter. One sees the archive, the other sees the working set. If your product reconciles against supplier records, the hidden ones are the rows that will look like data loss.
GET https://api.freeagent.com/v2/contacts?view=all&sort=updated_at&updated_since=2026-08-01T00:00:00.000ZUser-Agent: YourProduct/1.4 (support@yourproduct.example)Authorization: Bearer {access_token}Accept: application/json
Four things stated rather than assumed: the view, the sort, the delta window and the user agent. FreeAgent asks for the last of these explicitly, “A user-agent must be specified”, so that it can identify the application calling it. The line breaks are for reading.
The default order is by name, built from organisation_name, last_name and first_name, and created_at and updated_at are available, with a leading hyphen for descending. The default sort key is therefore a mutable field, and a record that moves between two page requests is a record a paging sync can miss. sort=updated_at is the steadier choice for a delta pass.
The nine views, the default, the status values and the sort orders from FreeAgent’s contacts reference, checked 17 August 2026.
Contacts waited eight years for a delta filter
Nothing calls you here. No FreeAgent object has an event enabled through Maesn today, so noticing that something changed is a read you schedule. What FreeAgent offers for that is updated_since, and its arrival date matters as much as its existence. Through Maesn that name stays underneath: the unified interface carries one delta filter and maps it onto whatever each system expects, so you write this loop once and it serves every system you add after this one.
The filter reached the resources one at a time rather than all at once, and FreeAgent’s changelog dates every step.
- 22 May 2017
updated_since on seven endpoints
invoices, bills, timeslips, estimates, expenses and the two bank resources
- 21 April 2025
updated_since on contacts
Eight years later, and contacts is the object you can write through Maesn
- 3 June 2025
Combinable with a date range
from_date and to_date may now be passed alongside it
Seven endpoints got the filter on 22 May 2017: invoices, bills, timeslips, estimates, expenses, bank transactions and bank transaction explanations. Contacts got it on 21 April 2025. On 3 June 2025 it became combinable with a date range, so from_date and to_date can now be passed alongside it.
The gap is the point. Contacts is the one object you can write through Maesn, and it is the one that waited longest to be able to answer what changed. Any integration built against this system before spring 2025 either re-read the whole contact list on every pass or tracked changes on its own side.
The operational consequence outlives the history. Because the filter arrived per endpoint, whether a given resource takes it is a question to check rather than assume, and a sync that sends updated_since to an endpoint that does not document it is a sync that quietly reads everything.
Attachments are a second case of the same care. As of 12 August 2025 they “require the same access level as the item they are attached to”, which folds the previous section into this one.
A scheduled read on a timestamp is the same shape of work as a subscription, minus the endpoint you have to host and secure. Where a system does send events the handling differs, and unified webhooks covers that side.
For FreeAgent the description that holds is a filter and an interval. What our own measurement supports is that nothing is switched on here today, rather than any statement about what the platform can do.
The three dates and the attachment rule from FreeAgent’s changelog, the filter itself from its contacts reference (the same page as above), both checked 17 August 2026. Object states from our own coverage data, which records what is enabled today rather than what FreeAgent can reach.
The identifier is a URL rather than a number
FreeAgent does not identify a record by a number. Its attribute tables give the url field a kind of URI and describe it as “The unique identifier for the contact”, and references between records carry that whole address rather than a foreign key.
Two things follow for your schema. A primary key column sized for an integer is the wrong column, and a reference you receive is not something to look up, it is something you could fetch. That reads as elegant until you try to compare two records, or store a key that has to survive a host change.
POST https://api.freeagent.com/v2/contactsHTTP/1.1 201 CreatedLocation: https://api.freeagent.com/v2/contacts/70{ "contact": {"url": "https://api.freeagent.com/v2/contacts/70","organisation_name": "Acme Ltd","updated_at": "2011-09-16T09:34:41Z"} }
The status, the Location header and the identifier are FreeAgent’s own documented response, trimmed to the fields under discussion. A client that reads only the body still gets the address, in url.
Through Maesn these resolve into stable identifiers in the common data model, so nothing downstream takes an address apart to find an id, and the same field means the same thing here as it does on a system that hands out integers. If you are building directly, the rule that saves the most rework is to treat the URL as opaque and never parse the trailing segment out of it.
One more shape to know about rather than plan for: every resource is offered in JSON and in XML, selected by the Accept and Content-Type headers, and the documentation prints both. It is a choice rather than a trap, and JSON is the one to make.
The URI kind, the create response and the Location header from FreeAgent’s contacts reference (the same page as above); the two formats from its introduction (as above), both checked 17 August 2026.
The headers carry the paging, the back-off and your identity
FreeAgent puts the payload in the body and the protocol in the headers. That is one habit rather than four separate quirks, and a client written against the body alone is wrong in four places at once.
| Header | Direction | What it carries |
|---|---|---|
| User-Agent | Request | Identifies your app. FreeAgent requires it |
| Authorization | Request | The bearer token |
| X-RateLimit-Test | Request | Lowers the sandbox to 5 requests a minute |
| Link | Response | The previous, next, first and last page |
| X-Total-Count | Response | How many records you can page over |
| Retry-After | Response | Seconds to wait after a 429 |
Paging is 25 records by default and 100 at most, and the navigation is in the Link header with prev, next, first and last. The check that catches a short sync is X-Total-Count, which reports how many records exist to page over. Compare it against what you stored and a truncated read stops being silent.
There are three request ceilings, not one: 120 requests a minute, 3.600 an hour and 15 token refreshes a minute. The counter that surprises people is the third, because a refresh is itself a rate-limited operation, so a scheduled job can fail at the authentication step before it asks for any data.
FreeAgent is explicit: “These limits are per individual user of your integration and are reset at the start of every hour/minute.” On the practice route it goes further, applying them “to individual clients” rather than to the firm as a whole.
So one customer’s sync cannot spend another customer’s budget. What you design against here is a single busy tenant rather than contention across all of them.
HTTP/1.1 429 Too Many RequestsDate: Wed, 27 Mar 2019 20:47:40 GMTRetry-After: 60You must not exceed 15 requests per 60 seconds
FreeAgent’s own example, including the date on it. The back-off is not optional in its wording either: it says it may “further restrict apps which do not respect the limits”.
The sandbox has a switch for testing exactly this. Sending X-RateLimit-Test with a truthy value lowers sandbox calls to 5 a minute, which turns the back-off path from something you hope works into something you can exercise. It runs on a separate host with its own credentials, so the sandbox is a second target system rather than a flag on the first.
Paging and cursors differ on every system in the catalogue, and unified pagination and filtering is where that is normalised into one shape. Through Maesn, the Link header parsing, the request budget and the 429 back-off sit behind the interface and what reaches your code is the complete set.
The three limits, the per-user sentence, the practice variation, the 429 example, the pagination defaults, X-Total-Count, the user agent requirement and the rate limit test header from FreeAgent’s introduction (as above); the sandbox host from its OAuth reference, both checked 17 August 2026.
The practice dashboard is a separate product and a separate App
FreeAgent sells to two audiences and serves them with two related products: online accounting for companies, and a practice dashboard for accountants and bookkeepers whose clients use FreeAgent. Reaching both is not a parameter.
The vendor states it plainly. The products are “two related but different products” and “You’ll need different API Apps to access both”. The practice route also needs the option enabled on the App, and every request carries an extra header naming the client subdomain you are acting for.
Which one you need is a question about your customers rather than your architecture. If they are businesses keeping their own books, the company product is the one. If they are practices reaching a portfolio of clients, it is the other, and the per-client rate limiting from the previous section is part of why that is a different scaling model rather than the same one with more rows.
What Maesn supports today is the company setup: one App, one callback address, one account key per connected customer. If your customers are practices, raise it before you register anything so the answer comes from us rather than from a rebuilt integration.
Authentication itself is a single redirect, and the reason is FreeAgent’s account model rather than anything on our side: a company account holds one set of books, so consent settles the only open question. Where a system does reach several companies or environments, the picker is part of the same unified flow and Maesn stores the answer on the connection. Either way what comes back to you is one account key, which is the point of doing it this way.
The authorisation code is valid for 15 minutes and the access token for an hour, and FreeAgent leaves the timing of a refresh to you: “It is not necessary to refresh the access token before the old access token expires.”
The two products and the two Apps from FreeAgent’s introduction (as above), the subdomain header and the App option from its accountancy practice reference, the token lifetimes and the refresh sentence from its OAuth reference (as above), all checked 17 August 2026. The two authentication lists are our own FreeAgent documentation.
What stays on your side
This is a small surface, and most of the work it creates is absorbable. Four things stop being yours:
- The header protocol. Link parsing, the total count check, the per-user request budget and the 429 back-off sit behind the interface, so a short read cannot reach your code as a complete one.
- URL identifiers. They are resolved into stable keys during normalisation, so no call site parses an address to find an id and no schema needs a column shaped like a web address.
- The party mapping. Two readable roles and one writable record arrive as the same fields you already read from every other system, rather than as a shape you special-case for this one.
- The token cycle. The hourly expiry and the refresh that counts against a rate limit are scheduled per tenant instead of on every call.
Three things do not move, and two of them are decisions rather than work:
- Which user connects. No interface can raise a permission grade in someone else’s FreeAgent account. If the fields below the divider matter to your product, that is a sentence in your onboarding instructions, and worth writing before a customer reports missing data.
- How often you look. Change detection is a scheduled read on a timestamp, so the interval, how far back each pass reaches and how much re-reading you accept stay yours to choose.
- Which view you mean. Whether your product wants the working set or the archive is a product question. The default answers one of them without being asked, and only you know which one is right.
On the objects themselves the connection is focused. Customers and suppliers are readable and contacts is writable, which carries the party layer end to end: you read the role, you write the record.
FreeAgent exposes a delete on the contact resource itself, and that operation sits in the same band as the rest of the 37 objects, available on request rather than absent. Which of them is switched on follows the use case, so it is worth naming the ones you need before your design is fixed.
Object states from our own coverage data, which records what is enabled today rather than what FreeAgent can technically reach. The documented delete from FreeAgent’s contacts reference (as above), checked 17 August 2026.
Frequently asked questions
What decides which fields a FreeAgent contact returns?
Why does a list of FreeAgent contacts come back short?
What is the difference between the clients and the suppliers view?
How do I detect changes in FreeAgent without events?
Why is a FreeAgent record identified by a URL?
Do I need a second application for accountancy practices?
Can I delete a FreeAgent contact through Maesn?

QuickBooks Online Webhooks: Events, Retries and Recovery
QuickBooks Online webhooks cover 29 entity types and expect HTTP 200 in three seconds. Why Intuit still asks you to poll change data capture.
Lennart Svensson · 25 Aug 2026
Lexware Office Pagination: The 406 and One Page Size
Lexware Office validates the page size and rejects a bad one with 406, the same code it uses for an unsupported media type. What that means for your read loop.
Lennart Svensson · 20 Aug 2026
How to Integrate with DATEV Rechnungswesen: One Connection
One connection carries reading and writing in DATEV Rechnungswesen, on a two-year token. Which objects travel in which direction is the real decision.
Lennart Svensson · 17 Aug 2026Build once on the Unified API.
FreeAgent decides what a read returns with a permission grade, a default view and the date an endpoint got its filter. The next system will draw those lines somewhere else, or not draw them at all. Build against one interface and each difference becomes a field you read instead of a branch you maintain.