"Sage" is not one product and not one API. Sage Group sells a family of accounting and ERP products that share a brand and almost nothing else, and picking the wrong one is the commonest mistake in a Sage integration brief. The three that matter for a commerce database are Sage Accounting (formerly Sage Business Cloud Accounting and before that Sage One), a small-business cloud ledger with an OAuth 2.0 REST API; Sage Intacct, a mid-market cloud financial system with an XML web services API that predates REST conventions entirely; and the ERP line, Sage X3 and Sage 200, which are larger, frequently on-premise, and reached through a mixture of REST, SOAP, GraphQL and OData depending on version and region. This page treats each separately and says plainly which is which.
At a glance
What it is
The Sage Group is a British software company, one of the oldest in the accounting software business, with products layered by customer size:
- Sage Accounting (
api.accounting.sage.com) is the small-business cloud ledger, strongest in the United Kingdom, Ireland, France, Spain, Germany, Canada and the United States. It competes with Xero and QuickBooks Online. - Sage Intacct (
api.intacct.com) is a mid-market cloud financial management system acquired by Sage in 2017, strongest in the United States, with multi-entity consolidation, dimensions, project accounting and an inventory and order-entry module. It competes with NetSuite. - Sage X3 is a full ERP for multi-entity manufacturers and distributors. Sage states that over 7,000 organisations in more than 80 countries use it.
- Sage 200 is a mid-range ERP, largely UK, Ireland and Spain, sold on-premise or on the Sage Partner Cloud. The API surface differs by region: the Spanish line publishes a Sage 200 Sales API "to expose the sales data from Sage 200 on-premise applications", and the UK line has its own separate API.
- Sage 50 and Sage 100 are older desktop or client-server products with their own SDKs, outside the scope of this page.
For a commerce database, Intacct is the one with a real inventory and order model, Sage Accounting is the one a small D2C brand is most likely to be on, and X3 or 200 mean a partner conversation.
The developer.sage.com documentation is served behind a Cloudflare challenge and rendered client-side. Every attempt to read the Accounting, X3 and 200 reference pages, directly and through a rendering proxy, returned either a challenge page or the portal's own 404 shell. What follows for Sage Accounting therefore comes from Sage's own published sample applications on GitHub, which are first-party but are sample code rather than a reference, and from the portal's public landing pages which did render. Confidence is marked accordingly, and the resource list for Sage Accounting is deliberately short rather than guessed at.
API access
Sage Accounting
Register an application in the Sage Developer Self Service console, which issues a client id and client secret and where you register the callback URL. Sage's own sample applications name a single scope, full_access, and version 3.1 of the API selected through a filter=apiv3.1 parameter on the authorization URL.
Endpoints, taken verbatim from Sage's official PHP sample application:
Sage publishes sample applications in PHP, Ruby and C# under its own GitHub organisation. The Ruby and PHP samples are marked deprecated in their READMEs but still carry the working endpoints and header set. There is no official server SDK.
Sage Intacct
Three things must be in place, and the third is the one that blocks integrations:
- A Web Services developer licence, which gives you a permanent sender id and sender password. This is a commercial arrangement with Sage, not a self-serve signup.
- A user in the customer's company with the right permissions, or a session obtained from one.
- The customer must subscribe to Web Services and explicitly authorise your sender id in their company configuration. Without that, every call from your sender id is rejected even with valid user credentials. Onboarding a new Intacct customer therefore always involves an administrator action inside their tenant.
Gateway: https://api.intacct.com/ia/xml/xmlgw.phtml, HTTP POST only. Intacct notes that HTTP GET is not supported and has been blocked since 15 January 2023. Content type is application/xml.
Official SDKs, all under Sage's intacct GitHub organisation: .NET, Node.js, PHP, and a work-in-progress Python. The intacct_dtd repository publishes the Web Services schemas, which is the authoritative contract.
Sage X3 and Sage 200
Sage describes X3 as offering "RESTful, SOAP, and native GraphQL APIs, all discoverable and extendable", with Sage X3 Builder as the tooling to extend the GraphQL schema, plus import and export templates for bulk data and a published X3 MCP server. Sage 200 publishes per-region REST APIs; the Spanish 200c line lists a Sage 200 Sales API at version 1.0 whose stated aim is "to expose the sales data from Sage 200 on-premise applications". Neither reference rendered for the fetcher, so no endpoint, field or authentication detail is asserted here.
Authentication
Sage Accounting: OAuth 2.0 plus X-Business
- Redirect to the authorization endpoint, which is on
sageone.comrather than on the API host, and carries afilterparameter selecting the API version.
GET https://www.sageone.com/oauth2/auth/central ?filter=apiv3.1 &response_type=code &client_id=<client_id> &redirect_uri=https://connector.example.com/callback &scope=full_access &state=<random>
Read the redirect. You get
code, yourstate, and acountryparameter naming the user's country, which Sage's Ruby sample stores as anapi_country_code. The country matters because Sage Accounting is localised per region.Exchange the code at the token endpoint. Client credentials go in the form body, not in a basic header.
curl -X POST 'https://oauth.accounting.sage.com/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=<client_id>' \ -d 'client_secret=<client_secret>' \ -d 'code=<code>' \ -d 'grant_type=authorization_code' \ -d 'redirect_uri=https://connector.example.com/callback'
The response carries access_token, refresh_token, an expiry, and a resource_owner_id. That last field is the business identifier and is what goes in the X-Business header on every call. Sage's Ruby sample stores exactly those four values.
Refresh with
grant_type=refresh_tokenand the same client id and secret in the body. Sage's samples track an absoluteexpires_attimestamp and refresh before it passes; the sample UI counts down in seconds, which suggests a short access token lifetime, but the exact number is not published on any page reachable here.Call the API with four headers, quoted from Sage's Ruby sample:
curl 'https://api.accounting.sage.com/v3.1/contacts' \ -H 'Authorization: Bearer <access_token>' \ -H 'Accept: */*' \ -H 'Content-Type: application/json' \ -H 'X-Business: <resource_owner_id>' \ -H 'User-Agent: my-connector/1.0'
X-Business is the tenant selector. One user may have several businesses, and the header decides which set of books the call reads or writes. Multi-account means storing a token pair per authorisation plus a set of business identifiers.
Sage Intacct: sender credentials plus a session
Intacct has two layers of identity in every request, which is unusual and is the thing to model carefully:
- Sender credentials (
senderidandpasswordinside<control>) identify your application. They are permanent and the same for every customer. - Authentication (inside
<operation>) identifies the user and company. Either a<login>block withuserid,companyidandpassword, optionallylocationidfor multi-entity companies, or a<sessionid>.
The documented practice is to log in once with getAPISession, take the session id, and use <sessionid> for everything afterwards. Intacct calls session authentication "the preferred way to make repeated API requests" and warns against repeated logins.
A request envelope, assembled from the documented <control> elements (the element names and order are those written by Sage's own PHP SDK):
<?xml version="1.0" encoding="UTF-8"?>
<request>
<control>
<senderid>my_sender_id</senderid>
<password>my_sender_password</password>
<controlid>req-2026-09-22-0001</controlid>
<uniqueid>false</uniqueid>
<dtdversion>3.0</dtdversion>
<includewhitespace>false</includewhitespace>
</control>
<operation>
<authentication>
<login>
<userid>webservicesuser</userid>
<companyid>acme-prod</companyid>
<password>userpassword</password>
</login>
</authentication>
<content>
<function controlid="fn1">
<getAPISession />
</function>
</content>
</operation>
</request>
Control block elements, as documented:
<operation> takes an optional transaction="true" attribute, under which "all of the functions are treated as a single transaction" and either all succeed or all roll back. That is the closest thing to a batch write with atomicity in this set of platforms, and it is genuinely useful.
Each <function> carries its own controlid attribute, which is distinct from the <controlid> element in the control block. The documentation is explicit: the element matches a request to a response, while the function attribute serves uniqueness verification.
Objects we can read
Sage Intacct
Reads use generic functions inside <content>. The three shapes:
<read>by key: give the object name, a comma-separated key list and a field list.<readByQuery>by filter expression, the older query form.<query>the newer, structured form with<select>,<filter>,<orderby>,<pagesize>and<offset>.
<query> pagination is documented precisely: pagesize defaults to 100 and has a maximum of 2000, and offset skips results. Filter operators include equalto, greaterthan, between, like, in and isnull.
<function controlid="q1">
<query>
<object>ARINVOICE</object>
<select>
<field>RECORDNO</field>
<field>CUSTOMERID</field>
<field>INVOICENO</field>
<field>DATECREATED</field>
<field>TOTALENTERED</field>
<field>STATE</field>
</select>
<filter>
<greaterthan>
<field>WHENMODIFIED</field>
<value>09/01/2026 00:00:00</value>
</greaterthan>
</filter>
<orderby>
<order><field>RECORDNO</field></order>
</orderby>
<pagesize>2000</pagesize>
<offset>0</offset>
</query>
</function>
Generic read and legacy object-specific functions differ in one trap the documentation calls out: transaction line indexing is zero-based in generic read results but one-based in legacy update functions such as update_sotransaction, so you must add one to the line number when feeding a read result into an update.
Orders
SODOCUMENT is the Order Entry transaction object, and it covers quotes, orders, invoices and credit memos, discriminated by document type. Documented fields include RECORDNO, DOCID, DOCUMENTNO, CUSTOMERID, DATECREATED, STATE (Draft, Submitted, Approved, Declined, Closed and others), PRRECORDKEY (the related AR transaction record number, which is the link from an order to its invoice), CURRENCY and BASECURR.
<read> <object>SODOCUMENT</object> <keys>1</keys> <fields>*</fields> </read>
Because STATE is a workflow position rather than a fulfilment status, and because the document type is configurable per customer (Sage Intacct customers define their own Order Entry document types), an Intacct connector cannot assume a fixed status vocabulary. Read the customer's document type configuration first.
Order items
SODOCUMENTENTRY holds the transaction lines, and SODOCUMENTSUBTOTALS holds the subtotal lines, which is where shipping, handling and tax charges live as separate records rather than as fields on the header. A total therefore needs both.
Products
ITEM. Documented fields: ITEMID (required, the SKU), NAME (required), ITEMTYPE (required, one of Inventory, Non-Inventory, Kit and others), PRODUCTLINEID, STATUS (active or inactive), STANDARD_COST and BASEPRICE. The item spans Inventory Control, Order Entry and Purchasing, and the item type decides which of those it can appear in.
<read> <object>ITEM</object> <keys>1</keys> <fields>*</fields> </read>
There is no listing object. BASEPRICE is one price; Intacct's price lists and contract pricing are separate objects not read here.
Inventory
Intacct is the only product in this set with real multi-warehouse stock. Inventory movements are INVDOCUMENT, with INVDOCUMENTENTRY lines and INVDOCUMENTSUBTOTALS. A line carries ITEMID, WAREHOUSEID, QUANTITY, UNIT and COST. Header fields include RECORDNO, STATE (Pending, Draft, Closed), TOTAL, DATECREATED, DOCUMENTNO, REFERENCENO and BASECURR.
Balances rather than movements come from Intacct's inventory total reporting objects. The exact object name for on-hand quantity per warehouse was not confirmed from a page that rendered here, so it is listed as an open question rather than asserted.
Returns and payments
Returns are Order Entry credit memo documents (another SODOCUMENT type) or AR credit memos. ARINVOICE is the receivable, with documented fields RECORDNO, CUSTOMERID, INVOICENO, DATECREATED, DATEPOSTED and DATEDUE, and lines nested as lineitem inside invoiceitems carrying accountlabel or glaccountno, amount, memo, locationid, departmentid, projectid and taxentries.
Payments are AR payment objects; settlements in the marketplace sense do not exist.
Customers and locations
CUSTOMER for customers, with the usual name, contact and address structure (all PII). Intacct's dimensional model gives you LOCATION, DEPARTMENT, PROJECT and CLASS as analysis dimensions that appear on almost every transaction line, and WAREHOUSE as the separate, physical stock-holding entity. Do not conflate LOCATION (an accounting entity or site) with WAREHOUSE (a stock location): in a multi-entity Intacct company they are different things and both appear.
Sage Accounting
The one resource name confirmed from Sage's own samples is contacts, used as the worked example in both the Ruby and PHP applications, with this create body:
{
"contact": {
"name": "Jane Doe",
"contact_type_ids": ["CUSTOMER"]
}
}
That shows the two conventions: a singular wrapper object around the body, and plural, snake_case resource paths appended to https://api.accounting.sage.com/v3.1/. contact_type_ids with a value of CUSTOMER is how Sage Accounting distinguishes customers from suppliers, the same both-in-one-object pattern Xero uses.
Beyond contacts, the reference that would list the sales invoice, product, stock item and payment resources did not render for any fetch attempt. Rather than guess at resource names, treat the catalogue as unverified: authenticate, then call the API root and follow what it returns, or read the reference from a browser session. Do not hard-code resource names taken from third-party wrappers without checking them against a live response, because several of the community SDKs for this API are unmaintained and target older versions.
Sage X3 and Sage 200
Not established. X3 exposes REST, SOAP and GraphQL and the GraphQL schema is extensible per customer through Sage X3 Builder, which means the object set is not fixed across installations. Sage 200's API surface is regional. Both require reading the reference from an authenticated browser session or a partner engagement.
Writing back: sales orders, invoices and stock adjustments
Sage Intacct
All three exist, and Intacct is the strongest of the Sage products here because it has a genuine inventory transaction.
Sales order or sales invoice, through the Order Entry legacy function:
<function controlid="so1">
<create_sotransaction>
<transactiontype>Sales Invoice</transactiontype>
<datecreated><year>2026</year><month>9</month><day>22</day></datecreated>
<customerid>C1000</customerid>
<documentno>docno001</documentno>
<sotransitems>
<sotransitem>
<itemid>R0044</itemid>
<quantity>1</quantity>
<price>100</price>
</sotransitem>
</sotransitems>
</create_sotransaction>
</function>
transactiontype names one of the customer's configured Order Entry document types, so Sales Invoice and Sales Order are conventional names, not guaranteed ones. Update by key:
<update_sotransaction key="Sales Order-SO1234">
<updatesotransitems>
<updatesotransitem line_num="1">
<memo>Updated memo</memo>
</updatesotransitem>
</updatesotransitems>
</update_sotransaction>
Note the key format, <document type>-<document number>, and the one-based line_num.
AR invoice, if you want the receivable without going through Order Entry:
<function controlid="inv1">
<create_invoice>
<customerid>CUSTOMER1</customerid>
<datecreated><year>2026</year><month>09</month><day>22</day></datecreated>
<invoiceno>234</invoiceno>
<invoiceitems>
<lineitem>
<accountlabel>TestBill Account1</accountlabel>
<amount>76343.43</amount>
<memo>Just another memo</memo>
</lineitem>
</invoiceitems>
</create_invoice>
</function>
Stock adjustment or transfer, through the Inventory Control transaction:
<function controlid="ic1">
<create_ictransaction>
<transactiontype>Inventory Transfer</transactiontype>
<datecreated><year>2026</year><month>9</month><day>22</day></datecreated>
<state>Pending</state>
<ictransitems>
<ictransitem>
<itemid>I123</itemid>
<warehouseid>W100</warehouseid>
<quantity>10</quantity>
<unit>Each</unit>
<cost>49.99</cost>
</ictransitem>
</ictransitems>
</create_ictransaction>
</function>
Like create_sotransaction, transactiontype names a customer-configured document type; "Inventory Transfer" is the documented example and an adjustment type is configured alongside it. state of Pending creates it unposted. Quantity here is a movement, not an absolute: Intacct has no set-stock-to-N operation, so an absolute target must be turned into a delta against the current on-hand figure.
Items and price. <create><ITEM>...</ITEM></create> with ITEMID, NAME and ITEMTYPE required; BASEPRICE and STANDARD_COST are ordinary updatable fields.
<create>
<ITEM>
<ITEMID>I1</ITEMID>
<NAME>hello world</NAME>
<ITEMTYPE>Inventory</ITEMTYPE>
</ITEM>
</create>
Idempotency and batching. Two mechanisms, both good. <control><uniqueid>true</uniqueid></control> makes a request non-repeatable, which is a real idempotency guarantee keyed on the control id. <operation transaction="true"> wraps every function in the request in one database transaction, so a multi-function write is all-or-nothing. Generic functions accept several objects in one call; legacy object-specific functions do not and need one call each.
Sage Accounting
Writes are ordinary REST. Sage's sample application does POST and PUT against https://api.accounting.sage.com/v3.1/<resource> with a JSON body wrapped in a singular key, the bearer token, and the X-Business header, exactly as shown under Authentication. The contact create above is the confirmed example. Invoice, product and stock resource names and their bodies could not be verified here.
Sage X3 and Sage 200
Not established.
Webhooks and notifications
No webhook or push notification mechanism was found for Sage Accounting, Sage Intacct, Sage X3 or Sage 200 in any source readable here. Sage Intacct's documentation describes policyid for asynchronous request processing, which is a way to submit a long-running request and collect the result later, not an event subscription. Treat all four products as polling targets.
Recommended polling shape for Sage Intacct: a <query> per object filtered on WHENMODIFIED greater than the last high-water mark, with pagesize 2000 and offset walking the result set. WHENMODIFIED and WHENCREATED are standard Intacct audit fields present on the objects in this page. Run it every 5 to 15 minutes for transactions and hourly for masters.
For Sage Accounting, poll the resource lists on a similar cadence. Whether the API exposes a modified-since filter could not be verified here; if it does not, the fallback is to page the full list and diff locally, which argues for a longer interval.
Rate limits and pagination
Sage Intacct. No published per-minute or per-day request limit was found on the pages readable here. What is documented is the pagination contract: <query> takes pagesize with a default of 100 and a maximum of 2000, and offset to skip. The legacy readByQuery form pages through readMore. The practical constraint is that Intacct is a large multi-tenant system and a very wide <select> with <fields>*</fields> over a 2000-row page is slow; select only the fields you need. policyid exists specifically so that a heavy request can be run asynchronously rather than held open.
Sage Accounting. Rate limits are published on a documentation page that did not render for any fetch attempt here, so no numbers are asserted. Sage's sample application shows a per-request timeout of 10 seconds on the OAuth client, which is a client setting rather than a server limit. Assume a limit exists, read the response headers on a live call, and build backoff on HTTP 429 before going to production.
Sage X3 and Sage 200. Not established.
Mapping to the unified model
Two columns, because the two main products have different field names. Sage X3 and Sage 200 are omitted rather than guessed at.
Gaps and open questions
- The Sage developer portal is unreadable to a machine. developer.sage.com sits behind a Cloudflare challenge and renders its documentation client-side. Repeated attempts against the Accounting, X3 and 200 reference and guide pages, directly and through a rendering proxy, returned a challenge page or the portal's own 404 shell. The Sage Accounting resource catalogue, rate limits, pagination model and webhook support are therefore all unverified. This is the single biggest gap on this page and should be closed by reading the reference in a browser or from an authenticated session.
- Sage Accounting token lifetimes. Sage's samples track an absolute expiry and display a countdown, so the access token is short-lived, but the exact number of seconds and the refresh token lifetime are not published anywhere reachable here.
- Sage Accounting resource names. Only
contactsis confirmed, from Sage's own sample code. Invoice, product, stock and payment resource names were deliberately not guessed. Several community wrappers exist for this API and most target version 3.0 or earlier and are unmaintained, so they are not a safe substitute for the reference. - Sage Intacct rate limits. Not published on the pages read. The
policyidasynchronous mechanism suggests Intacct expects heavy requests to be offloaded rather than throttled, but a connector should still implement backoff. - Sage Intacct inventory balance object. Movements through
INVDOCUMENTare confirmed. The object that reports on-hand quantity per warehouse was not confirmed from a page that rendered, and should be established before designing an inventory sync. - Customer-configured document types. Both
create_sotransactionandcreate_ictransactiontake atransactiontypethat names a document type the customer has configured.Sales Invoice,Sales OrderandInventory Transferare the documented examples, not a fixed enum. A connector must read each customer's configuration rather than hard-code names. - Sage Intacct REST. Sage has been adding a REST surface to Intacct alongside the XML gateway. Its coverage, versioning and whether it supersedes XML for new integrations was not established here and is worth checking before committing to the XML gateway for a new build.
- No webhooks anywhere. Nothing in the sources read describes an event or push notification mechanism for any Sage product. If one exists for Sage Accounting it is on a page that did not render.
- Sage X3 and Sage 200 are a partner conversation. X3's GraphQL schema is customer-extensible, so there is no fixed object set, and Sage 200's API differs by region. Neither can be integrated from published documentation alone.
Sources
Sage Intacct, read directly:
- Sage Intacct Web Services overview: the
https://api.intacct.com/ia/xml/xmlgw.phtmlgateway, POST-only since 15 January 2023, the licence and sender-id authorisation requirements, and the login versus session choice - XML requests: the control block elements
senderid,password,controlid,uniqueid,dtdversion,includewhitespace,policyid,showadditionalerrorinfo, thetransactionattribute onoperation, and the distinction between the control id element and the function control id attribute - XML functions: generic versus object-specific functions, and the zero-based versus one-based line indexing trap
- Queries: the
queryelement withobject,select,filter,orderby,pagesize(default 100, maximum 2000) andoffset, and the filter operator list - Order Entry transactions:
SODOCUMENT, its fields,SODOCUMENTENTRY,SODOCUMENTSUBTOTALS, and thecreate_sotransactionandupdate_sotransactionsamples - Items: the
ITEMobject, required and optional fields, and the create and read samples - AR invoices:
ARINVOICE, its fields, and thecreate_invoicesample - Inventory transactions:
INVDOCUMENT,INVDOCUMENTENTRY, warehouse and quantity fields, and thecreate_ictransactionsample - intacct/intacct-sdk-php, source read:
src/Intacct/Credentials/Endpoint.phpfor the default gateway URL andsrc/Intacct/Xml/Request/ControlBlock.phpfor the exact control element names and order - intacct/intacct_dtd: the published Web Services schemas
Sage Accounting, from Sage's own GitHub organisation because the portal was unreachable:
- Sage/sageone_api_php_sample, source read:
lib/api_client.phpforBASE_ENDPOINT,AUTH_ENDPOINT,TOKEN_ENDPOINTandSCOPE - Sage/sageone_api_ruby_sample, source read:
app/controllers/sage_accounting_controller.rbfor the authorization URL parameters, the token exchange and refresh bodies, theresource_owner_idfield, and theAuthorization,Accept,Content-Type,X-BusinessandUser-Agentheader set;app/views/sage_accounting/_post.html.erbfor thecontactsrequest body shape - Sage/sageone_api_csharp_sample
Sage portal landing pages that did render:
- developer.sage.com/accounting: the Sage Accounting API landing page
- developer.sage.com/x3: REST, SOAP and GraphQL, Sage X3 Builder, the X3 MCP server, and the 7,000 organisations in 80-plus countries figure
- developer.sage.com/200c: the Sage 200 API index, including the Sage 200 Sales API v1.0 and its stated aim of exposing sales data from on-premise Sage 200