---
name: sage-accounting-oauth-app
description: >-
  Creates or signs in to a Sage App Registry account and registers a Sage
  Accounting app to obtain an OAuth2 client ID and secret — covering the
  UK/IE/CA country routing that decides which authorization server a customer
  hits, the `readonly` / `full_access` scopes, the `X-Business` header that
  picks which business you read, five-minute access tokens with single-use
  refresh tokens that die after 31 days, and Marketplace validation. Use when
  asked to get Sage Accounting or Sage Business Cloud Accounting OAuth
  credentials, set up a Sage App Registry account, create a Sage Accounting
  app, extend a trial business, rotate a client secret, or fix a Sage OAuth
  error like `invalid_scope`, `invalid_grant`, an unregistered callback URL,
  or data from the wrong business. Sage Accounting only: Sage Intacct, Sage
  50/100/200, Sage X3 and Sage Active are different products with different
  portals.
---

# Sage Accounting OAuth2 App Registration

Get a working Sage Accounting OAuth2 client — a Sage App Registry account, a registered app, callback URLs, a client ID
and a client secret — for a platform that connects many customers' Sage Accounting businesses.

Sage's registration form is the easiest part of this job. Four things outside the form decide whether the integration
works, and all four are silent failures.

**Sage Accounting is sold per country, and the country is part of the auth flow.** The v3.1 API serves exactly three
countries — the UK, Ireland and Canada — behind one API host but **two regional authorization servers**. If the country
the user picks does not match the country their business is registered in, authorization fails. That is §7.

**One token reaches every business the user can see, and you get the wrong one by default.** A Sage access token belongs
to a *user*, not a business. Unless you send an `X-Business` header, every call resolves against that user's *lead
business* — the first business they ever created. For an accountant on Partner Edition with forty client businesses,
that is the accountant's own practice file. That is §8.

**The tokens are the shortest of any major accounting API.** Access tokens last **five minutes**. Refresh tokens are
**single-use**, rotate on every refresh, and expire **31 days** after issue. A connector that refreshes lazily, or that
fires two concurrent refreshes, breaks. That is §9.

**Scopes are two words, not a list.** Sage offers `readonly` or `full_access`. There is no per-object scope, so the
consent screen a customer sees is effectively "read everything" or "read and write everything". That is §5, and it is a
conversation with the customer, not a configuration choice.

## Which Sage product this is

Sage sells many things with similar names and they do not share a developer portal, an auth model or an API.

| This skill covers | This skill does **not** cover |
| --- | --- |
| **Sage Accounting** (formerly Sage Business Cloud Accounting, formerly Sage One), **API v3.1**, host `https://api.accounting.sage.com/v3.1`, OAuth2 authorization code, countries **GB / IE / CA** | **Sage Intacct** — separate portal, separate REST API and OAuth model |
| Includes the **Accounting Start** and **Accounting Plus** variants of the same product | **Sage 50**, **Sage 100**, **Sage 200**, **Sage X3** — desktop/mid-market products, not this API |
| | **Sage Active**, **Sage Payroll** — separate products and portals |
| | **Sage Accounting South Africa** — a different regional product on **API v2.0.0** using **Basic Authentication**, its own enrolment form and its own API key portal. Not OAuth2, not this app |
| | Regional Accounting versions for **Australia, New Zealand and Asia (HK, SG, MY)** — Sage documents these as separate and outside the v3.1 guide |
| | **API v3.0**, which differed regionally across UKI, US, DE, ES, CA and FR — see **Platform state** |

If the user says "Sage" and means a US, German, French, Spanish, Australian or South African business, stop and confirm
which product before registering anything. A v3.1 app cannot serve them.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name** | Shown to Sage Accounting users on the consent screen (§3) |
| **Contact email address** | Required; Sage uses it to reach you about the app. Not shown to users (§3) |
| **Homepage URL** | Optional, but shown to users when given (§3) |
| **Callback URLs** | Every callback host your platform serves. **Case sensitive** (§4) |
| **App logo** | Optional, 250×250 recommended, shown during authorization (§3) |
| **Which countries** customers will be in | GB, IE and CA are the only ones v3.1 serves, and it changes the authorize URL (§7) |
| **Read-only or read-write** | Sage has exactly these two scopes; there is no middle ground (§5) |
| **Will customers have more than one business?** | Accountants and bookkeepers almost always do. Blocking design question (§8) |
| **Does the client store the rotated refresh token, and serialize refreshes?** | Blocking — a client that does neither dies within days (§9) |
| **Is a Sage Marketplace listing wanted?** | Triggers Sage's validation call and co-branding requirements (§11) |
| **New app, or an edit to an existing one?** | New credentials orphan every existing customer connection (§1) |

## Quick Start

1. Confirm this is **Sage Accounting**, not another Sage product (see the table above).
2. Confirm a **new app** is actually needed — existing connections are bound to the current client ID (§1).
3. Create the **App Registry** account, and separately a **trial Accounting business** to test with (§2).
4. Register the app and capture the client ID and secret (§3, §6).
5. Add **every** callback URL, byte-for-byte, case included (§4).
6. Decide `readonly` vs `full_access` with the customer, not for them (§5).
7. Pin the `country` parameter, or build a country picker — this is the top cause of failed connects (§7).
8. Decide how a customer picks a business, and send `X-Business` on every call (§8).
9. Confirm the client handles 5-minute access tokens and single-use, 31-day rotating refresh tokens (§9).
10. Verify a real authorize → callback → token → refresh → refresh-again round trip (§12).
11. Hand the credentials over — never commit them (§14).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **Sage rebuilt the Accounting developer docs.** The old `developer.sage.com/accounting/guides/...` and
  `developer.sage.com/accounting/quick-start/...` paths now return the portal's 404 page. The live tree is
  `developer.sage.com/accounting/docs/v1.0.0/guides/...`. Any runbook still linking the old paths is stale, and its
  content may be too.
- **`developer.sage.com` is behind Cloudflare bot protection and returns HTTP 403 to non-browser clients**, including
  `curl` with full browser headers. Every documentation page also publishes a plain-Markdown twin at the same path plus
  `.md` (the portal exposes it as "View as markdown"), and **those return HTTP 200 and are machine-readable**. The
  References section below uses the `.md` form for exactly this reason. If you need to check a page from a script, add
  `.md`; if you need to show it to a human, drop it.
- **API v3 was deprecated on 31 July 2026** (announced 21 July 2025 on the Accounting announcements page). v1 and v2
  were deprecated earlier. **v3.1 is the only version to build against**, and it is the version that unified the
  per-country base URLs into the single host `https://api.accounting.sage.com/v3.1`.
- **v3.1 serves three countries: `gb`, `ie`, `ca`.** Sage's own regional guide describes v3.1 as the "Global Accounting
  Core" API for the Northern Hemisphere and names UK, IE and CA. The same guide states that South Africa runs a separate
  v2.0.0 product on Basic Auth, and that Australia, New Zealand and Asia have separate regional versions it does not
  cover.
- **Trial businesses are currently UK-only.** Sage's quick-start lists trial sign-up links for United Kingdom across
  Accounting Start, Standard and Plus, and marks **Canada and Ireland as "Currently Unavailable"**. If you need an IE or
  CA test business today, that is a request to Sage, not a self-service step — raise it early.
- **A developer trial business can be extended to 12 months free**, via a form on Sage's quick-start that asks for the
  registration email, app name, client ID and country (Canada / Ireland / United Kingdom only). Sage aims to confirm in
  **3–5 working days**. Without the extension you are on the ordinary commercial Accounting trial and the business will
  lapse; Sage does not publish the base trial length in the developer docs, so confirm it on the sign-up page for the
  region you use rather than assuming.
- **PKCE is supported but optional.** `code_challenge` and `code_challenge_method` (`S256` or `plain`) are documented
  optional authorize parameters; `code_verifier` is then required at the exchange.
- **There is no documented review gate before third parties can connect.** Sage states that no enrolment is required
  prior to developing, and that terms are agreed regionally *prior to review of your application* — which is the
  Marketplace listing path (§11), not a prerequisite for a private integration. Do not promise a customer either way
  without checking §11.
- **Sign-in runs through Sage Identity (`id.sage.com`, Auth0)** with username → password → MFA OTP, before the consent
  screen. The consent screen appears **only** on the first authorization, after tokens are revoked, or after the user
  removes the app from Sage's "Manage Apps and Connections" page. A silent re-authorization is normal, not a bug.
- **Partner Edition API support is documented as still in development**, even though Partner Edition users' businesses
  do show up on the businesses endpoint (§8).

If the portal does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new app means a **new client ID, and every existing customer connection is bound to the old one** — every customer
re-authorizes, and each re-authorization is an action inside their Sage business that you cannot perform for them.

Reuse the existing app for: adding a callback URL, changing the app name or logo, rotating a compromised secret, or
diagnosing a failure. Sage lets you edit name, homepage and callback URLs in place.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app for
a different product, or a deliberate test app. Say which path you are taking before you touch the portal.

## 2. Two accounts, and they are not interchangeable

This is the single most common first-time failure, and Sage calls it out twice in its own docs.

| Account | What it is | Where |
| --- | --- | --- |
| **App Registry / developer self-service account** | Registers and manages apps, holds the client ID and secret, holds the callback URLs. **Free.** | `https://developerselfservice.sageone.com/` |
| **Sage Accounting business user** | The account that actually *authorizes* your app and owns the accounting data | `https://app.sbc.sage.com/` |

**Signing in with App Registry credentials during the OAuth flow fails.** The credentials the flow wants are those of a
registered user of a Sage Accounting business — including a freshly created trial business.

Creating the developer account:

- Sign up at `https://developerselfservice.sageone.com/` with **GitHub** or **email**. Email signup asks for email,
  name, phone number and a password, then sends a confirmation link.
- The verification mail comes from `donotreply@sageone.com`. Sage explicitly says to safelist that address **before**
  registering — a spam-filtered verification mail is a dead end that looks like a broken signup.
- GitHub signup requires a public email on the GitHub profile, or Sage asks for one separately.

Creating a test business:

- Create a **trial Sage Accounting business** for the region you will test against (UK only, today — see
  **Platform state**), then request the **12-month developer extension** (§ Platform state).
- Sage's own tip: use an email provider that supports plus-addressing or aliases, so several trial businesses live in
  one inbox. You will want more than one.
- Check the business is live and you can reach its dashboard *before* attempting OAuth.

Hand control back to the user for anything only a human can do: email verification, MFA enrolment and OTP entry,
accepting terms, creating the trial business, and submitting the trial-extension form. Do not retry a blocked step in a
loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand the
user an exact, ordered click path with the literal values to paste — the callback URLs from §4 and the scope decision
from §5 — then continue once they report back with the client ID.

## 3. Create the app

In the App Registry, click **Create App**. Four fields:

| Field | Required | Notes |
| --- | --- | --- |
| **App Name** | yes | **Displayed to Sage Accounting users when they authorize.** Treat it as go-to-market copy |
| **Email Address** | yes | Sage contacts you here about app issues. Not shown to users |
| **Homepage URL** | no | Shown to users when given |
| **Callback URLs** | yes, at least one | Up to **100**, one per line (§4) |

After saving, open the app to reach **App Details**, where the pencil icon edits name/homepage/callbacks and **Show**
reveals the client ID and secret (§6). You can also upload a **symbol image** shown during authorization; Sage
recommends **250 × 250 pixels**.

There is no app-type choice, no confidential/public toggle, and no sandbox/production split. One app, one credential
pair, and the same endpoints for trial and paying businesses — which is why §7's country parameter and §12's real
round-trip matter more here than a key-environment check would elsewhere.

## 4. Callback URLs

Register **every** callback host your platform serves. For Unified.to these are one per data center; confirm the current
list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Rules, all documented by Sage:

- **Up to 100 callback URLs per app**, at least one, **one per line**.
- **`https://` is required**, with `http://` allowed only for local hosts.
- **Callback URLs are case sensitive** and must match the `redirect_uri` in the authorize request *exactly*. Sage says
  this in both the quick-start and the OAuth troubleshooting guide, and lists it first among common issues both times.
- The `redirect_uri` sent at the token exchange must be the registered value **with no additional parameters appended**.
- If you test with Postman, register Postman's own callback URL as well — Sage documents this explicitly.

An unregistered or mis-cased callback URL fails at the *start* of the flow, on Sage's side, before the user ever sees a
login box.

## 5. Scopes: two words, and that is the whole vocabulary

Sage Accounting has no per-object scopes. The `scope` parameter is **optional** on the authorize request and takes one
of exactly two values:

| Scope | Meaning |
| --- | --- |
| `readonly` | Read access |
| `full_access` | Read and write |

Consequences worth saying out loud before a customer sees the consent screen:

- **There is no "invoices only" or "contacts only".** An integration that writes anything asks for `full_access`, which
  is read *and write* across everything the API exposes. If a customer's security review expects granular scopes, that
  conversation happens now, not at go-live.
- **Send one value.** Sage documents the parameter as "Can be `readonly` or `full_access`", and `invalid_scope` as the
  error when the value "is invalid or unknown", with the advice to ensure the scope "is either `readonly` or
  `full_access`". A client that concatenates both into a space-delimited list is not sending a documented value — treat
  that as a defect to check for (§13), not as a supported way to ask for both.
- The granted scope comes back in the token response as `scope`, so you can assert what you actually got rather than
  what you asked for.
- **What the token can do is still bounded by the user.** The access token carries the rights of the Sage Accounting
  user who authorized, and Sage stamps that user's initials on transactions the API creates. Sage's best-practice guide
  is explicit: in a multi-user integration, hold **per-user** tokens, or every transaction your app writes will be
  attributed to whichever single user happened to connect.

## 6. Capture the credentials and endpoints

From **App Details**, click **Show** on Client ID and Client Secret; a clipboard icon copies each. Unlike portals that
reveal a secret once, Sage's App Registry lets you re-read both.

Endpoints — the same for every country, and for trial and paying businesses alike:

| Purpose | URL |
| --- | --- |
| Authorize | `https://www.sageone.com/oauth2/auth/central?filter=apiv3.1` |
| Token (exchange and refresh) | `https://oauth.accounting.sage.com/token` |
| Revoke (refresh tokens only) | `https://oauth.accounting.sage.com/revoke` |
| API base | `https://api.accounting.sage.com/v3.1` |

Mechanics that catch clients out:

- Both the exchange and the refresh are **form POSTs** with `Content-Type: application/x-www-form-urlencoded` and
  `Accept: application/json`, carrying **`client_id` and `client_secret` in the body** — not an HTTP Basic header. Sage
  lists "the required header parameters have not been set" among its common OAuth failures.
- **Sage client IDs can contain a `/`.** Sage's own worked example is two UUIDs joined by a slash, URL-encoded as `%2F`
  in the authorize URL. A client that interpolates the client ID into a URL without encoding it will build a broken
  authorize link.
- `filter=apiv3.1` on the authorize URL restricts the country list to countries that support v3.1. Sage notes it is
  **ignored when a `country` is supplied** (§7).
- **Revocation takes the refresh token, and only the refresh token.** Access tokens cannot be revoked — they are valid
  for their full five minutes no matter what. Revoking returns `{"success": "ok"}` with HTTP 200, and the user must
  re-authorize afterwards.

Rotating the client secret breaks every exchange and refresh until the new value is deployed. Issued access tokens live
out their five minutes. Never rotate without explicit go-ahead and a cutover plan.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be regenerated, then move on.

## 7. Country and region: the routing trap

Sage Accounting is sold per country, and the authorize request is where that shows up.

The authorize URL's optional **`country`** parameter takes `ca`, `gb` or `ie`. There is also an optional **`locale`**
(e.g. `en-CA`, `en-GB`), which may be combined with `country`.

What happens when you omit it:

1. The user first sees a **country/region selection screen**.
2. Their choice routes the request to **one of two authorization servers — UK or North America**.
3. **If the country they pick does not match the country their business is registered in, authorization fails.** Sage
   gives the exact example: pass or pick `GB` for a business registered in the US and the auth fails.

Sage's own guidance, which is also the right architecture:

- If your integration supports **one** country, **always pass `country`**. It skips the selection screen and removes
  the whole class of failure.
- If you support **some but not all**, put a country picker in *your* UI and pass the result.
- The callback carries the country back: the redirect looks like
  `...?code=GB%2F12abc...&country=GB&state=random_string`. Note that the **authorization code itself is prefixed with
  the region and URL-encoded** — a client that fails to decode `%2F` back to `/` before the exchange sends a code Sage
  does not recognise.

Region differences do not stop at auth. Sage documents **endpoint and attribute availability varying by region and by
variant** — for example, corrective-invoice endpoints marked "Spain only" in the v3.1 reference, and inventory requiring
Accounting Plus. A connector tested against a UK Accounting Plus business can legitimately 404 or return a different
shape against an Irish Accounting Start business. Check the per-endpoint region/variant notes in the API reference
before promising coverage.

## 8. `X-Business`: one token, many businesses, and a default that will bite you

An access token belongs to a **user**, and a user may have access to many businesses. Sage's model:

```
GET /businesses          # every business this user can reach
GET /businesses/lead     # the "lead" business — the first one they created
GET /businesses/{id}     # one business, including its subscription variant
```

Every other request may carry a header naming the business it applies to:

```
X-Business: <business_id>
```

The rules, straight from Sage:

- **If you omit `X-Business`, the API reads and writes the lead business.** Not an error. Not a warning. The wrong
  business, silently.
- **The lead business can change.** Sage names two ways: the user deletes their first-created business, or a user who
  was previously only invited to businesses creates one of their own. Both re-point a connector that relies on the
  default.
- **You cannot tell from the authorization response whether the user has more than one business.** You must call the
  businesses endpoint after connecting. Sage says this explicitly, twice.
- **Every response except `/businesses` and `/user` returns an `X-Business` response header** naming the business the
  request actually resolved against. Store it on the first call and compare on later ones — that is Sage's documented
  way to detect a lead-business change.
- **Sage's stated best practice is to always send `X-Business` in production.** The lead-business default is described
  as a development convenience.
- **Partner Edition makes this acute.** When an accountant or bookkeeper on Partner Edition authorizes, the API can
  reach **every** business under their Active tab — including demo businesses — and the **Partner Edition business is
  returned as the lead**. Without a business picker, an accountant connecting your app syncs their own practice file
  instead of their client's. Sage calls providing that selection "imperative".
- **Business ids are your join key.** Sage's best-practice guide is blunt that any local cache must key on business id,
  so two businesses cannot be merged into one local representation.
- Since **July 2025** the businesses endpoint supports `active_only`, `name` and `product_family`
  (`sbc_accounting` / `sbc_payroll`) filters plus pagination. Pagination has sharp edges: `items_per_page` below 25 is
  forced to 25; `page` without `items_per_page` uses the maximum of 500; and `page` greater than 1 with fewer than 500
  results returns an empty array.
- Check `subscriptions[].active` is `true` before completing a connection, and read the subscription id to learn the
  variant: `START`, `MICRO` (Partner Edition), `ACCOUNTS` (pre-May 2020 Standard), `ACCOUNTING` (post-May 2020
  Standard), `ACCOUNTING_10` (Plus). Endpoint availability differs between them.

## 9. Token lifetimes: five minutes, 31 days, single use

| Token | Lifetime | Rule |
| --- | --- | --- |
| authorization `code` | **60 seconds**, single use | Exchange it immediately. Reuse or delay gives `invalid_grant` |
| `access_token` | **300 seconds / 5 minutes** (`expires_in: 300`) | Cannot be revoked. Reserve **2048 bytes** to store it |
| `refresh_token` | **2,678,400 seconds / 31 days** (`refresh_token_expires_in`) | **Single use.** Every refresh returns a new one. Reserve **2048 bytes** |

The token response also carries `requested_by_id` — the id of the user who authorized — and `scope`, the granted access
level (§5).

Four ways this kills an integration:

1. **Rotation.** The refresh token is consumed by the refresh that uses it. Persist the new one in the same transaction
   as the new access token. Sage lists four separate causes for a rejected refresh token: it expired, the user revoked
   access, **it has been used before**, or **it is not the most recent token the auth service issued**.
2. **Concurrency.** Because rotation is strict, two parallel requests that both notice an expired access token will both
   refresh, and one of them poisons the other. Sage devotes a section of its troubleshooting guide to this and tells you
   to serialize refreshes behind a single-flight lock. With a **five-minute** access token this is not a rare race; it is
   the normal case for any worker pool.
3. **The 31-day wall.** The window resets on every refresh, so an actively-synced connection lives indefinitely — but a
   connection nobody touches for 31 days is gone, and the customer must re-authorize. Compute and store
   `now + refresh_token_expires_in` at every refresh so you can warn before it lapses rather than after.
4. **There is no access-token revocation.** Revoking kills the refresh token; the outstanding access token keeps working
   until its five minutes are up. Plan disconnect flows accordingly.

## 10. Rate limits

Set **per app**, and — per Sage's own FAQ — **against the user and the app**, so a user with two businesses splits one
budget across both:

- **1,296,000 requests per app per day** (which is 15/second sustained).
- **150 concurrent requests** at any time, per app.
- Exceeding either returns **HTTP 429**.

Sage recommends wait-and-repeat handling and, more usefully, **queueing requests** so your app controls its own rate
rather than discovering the ceiling. The daily number is generous; the **concurrency cap is the one a parallel backfill
hits first**.

## 11. Validation, the Marketplace, and what is actually gated

Sage separates "can other organizations connect to my app" from "is my app listed on the Sage Marketplace", and only the
second has a documented process.

- **No enrolment is required before developing.** Sage's regional guide states this plainly, and contrasts it with the
  South African v2.0.0 product, which *does* gate production API keys behind a process.
- **Marketplace listing runs through a validation call.** Sage invites you to a recorded online meeting (usually Teams)
  to demo the integration. The review covers: does the integration perform as advertised; is Sage branding used
  correctly; what data is read from and written to Accounting; how that data is secured in application and database;
  where the data is hosted; and the customer experience.
- **Terms are accepted per region**, and a designated company admin is then invited to the Marketplace platform, with an
  offered 30-minute onboarding call. The listing carries privacy policy and T&Cs links, logos, target industries and
  categories, benefits, features and screenshots; editions and pricing are optional.
- **A co-branded landing page on your own site is required** for the listing to point at.
- Sage product logos are supplied as a partner pack and their use is bound by Sage's co-marketing guidelines, which you
  must accept before download.

None of this is a step you can perform. Demo scheduling, data-hosting and security answers, legal terms and marketing
copy are the platform owner's, not yours. Surface the requirement and hand back (see **Stop and ask**).

## 12. Verify end-to-end

Authorizing one UK trial business proves almost nothing about a production Irish accountant with nine clients. Work the
list.

1. Run the full authorize → callback → exchange against a trial business, **with `country` pinned** (§7).
2. Confirm the callback carried `code`, `country` and your `state`, that `state` matches, and that your client
   **URL-decoded** the region-prefixed code before exchanging (§7).
3. Confirm the token response carried `access_token`, `refresh_token`, `expires_in` 300, `refresh_token_expires_in`
   2678400 and the `scope` you expected (§9).
4. Call the businesses endpoint. Confirm your platform lists them and lets a human choose, and that **every** subsequent
   call sends `X-Business` (§8).
5. Read back the `X-Business` **response** header on a data call and confirm it equals the business you selected (§8).
6. **Force a refresh, then force a second refresh using the token the first one returned.** This is the step that
   catches a client ignoring rotation. Nothing else catches it before production does.
7. **Fire two refreshes concurrently** and confirm only one reaches Sage. With a five-minute access token, this will
   happen in production on day one (§9).
8. If customers may be accountants, authorize once from a **Partner Edition** login and confirm you do not silently sync
   the practice file (§8).

| Symptom | Cause |
| --- | --- |
| Flow fails before any login screen | Callback URL not registered, or registered with different casing (§4) |
| Sage rejects the login with the developer's own credentials | App Registry account used instead of an Accounting business user (§2) |
| `invalid_scope` on the authorize URL | `scope` is not exactly `readonly` or `full_access` — check for a concatenated list (§5) |
| `unsupported_response_type` | `response_type` is not `code` (§6) |
| `unauthorized_client` at authorize | Wrong or unknown `client_id` — check `/` encoding in the client ID (§6) |
| `access_denied` | The user declined on the consent screen — not a bug |
| Auth fails right after the country/region screen | Selected country ≠ the country the business is registered in (§7) |
| `invalid_grant` immediately after consent | Code expired (**60 seconds**), already used, or not URL-decoded (§7, §9) |
| `invalid_client` at the exchange | Wrong client ID or secret, or credentials sent as a header instead of form fields (§6) |
| Exchange fails with a valid-looking code | `redirect_uri` at exchange differs from the registered value, or has extra params appended (§4) |
| Everything works, then 401s five minutes later | Access token expired; refresh not wired up (§9) |
| First refresh works, second fails `invalid_grant` | Rotated refresh token not persisted (§9) |
| Refreshes fail intermittently under load | Concurrent refreshes — the older token is already consumed (§9) |
| Connection dies after a quiet month | 31-day refresh-token expiry; the customer must re-authorize (§9) |
| Data is real but from the wrong company | `X-Business` omitted, so the lead business answered (§8) |
| A connection that worked starts returning another business | The user's lead business changed (§8) |
| An accountant connects and you sync their practice, not their client | Partner Edition lead business; no business picker (§8) |
| An endpoint 404s for one customer only | Region or variant does not support it — check the reference's region/variant notes (§7) |
| HTTP 429 | 1,296,000/day or 150 concurrent, per app, shared across the user's businesses (§10) |
| All transactions your app creates show one person's initials | Single shared user token in a multi-user integration (§5) |
| The consent screen does not reappear on reconnect | Expected — it shows only on first auth, after revocation, or after the user removes the app (Platform state) |

## 13. Product fact: how this platform's Sage Accounting connector is wired

**As of 2026-09-20**, and worth confirming with the connector's owner before you register anything:

- The connector carries **three country profiles — CA, UK and IE** — that share the API base
  `https://api.accounting.sage.com/v3.1` and the token endpoint `https://oauth.accounting.sage.com/token`, and differ
  only in the authorize URL's `country` value (`ca`, `gb`, `ie`), each pinned with `filter=apiv3.1`. **The country is
  chosen when the connection is created, not detected** — so a customer routed to the wrong profile hits §7's failure.
  There is no US, ZA, AU or Asia profile, matching what v3.1 supports.
- The authorize request sends `client_id`, `redirect_uri`, `response_type`, `state` and `scope`, space-delimited.
  **PKCE is not used**, which Sage permits.
- The token exchange is a form POST carrying `client_id`, `client_secret`, `redirect_uri`, `code` and `grant_type`
  **in the body**; refresh sends `client_id`, `client_secret`, `refresh_token` and `grant_type`. Both match Sage's
  documented shape (§6).
- Scopes are mapped **per unified object**: every read maps to `readonly`, every write to `full_access`, and the
  identity/login leg asks for `readonly`. The connector is configured to drop a read scope when the matching write scope
  is also present. **Check this behaviour against §5 before trusting it**: Sage's two values are not a read/write pair
  of the same word, so a mixed read-and-write permission set can plausibly emit `scope=readonly full_access` — two
  values where Sage documents one, and `invalid_scope` is the documented response to an unrecognised value. Worth
  reproducing with a mixed permission set before a customer does.
- **It is not configured to use this platform's shared OAuth credentials.** Each deployment needs its own registered
  Sage app — which is why this skill exists.
- **No business-selection header is sent on any request.** Every object other than the organization listing therefore
  resolves against the authorizing user's **lead business** (§8). The organization object, by contrast, lists the
  businesses endpoint, so it returns **all** businesses the user can reach while every other object reads only one of
  them. For a customer with several businesses — and for any accountant on Partner Edition — that is a real mismatch:
  raise it with the connector's owner before promising multi-business behaviour, and model each business as its own
  connection in the meantime.
- **The revocation endpoint is not wired up**, so disconnecting on this platform does not invalidate the refresh token
  on Sage's side; the customer clears it from Sage's Manage Apps and Connections page.
- Listing is page-based and 1-indexed with an items-per-page parameter, and incremental sync uses Sage's
  `updated_or_created_since` filter.
- Coverage spans accounting (accounts, contacts, invoices, bills, journals, transactions, tax rates, trial balance,
  organizations, bank-feed accounts), commerce (items, collections, inventory), payments and file storage.

This is a snapshot of a codebase that changes. It is a starting point for the conversation, not a specification.

## 14. Hand off — never commit the secret

- **Do not** write the client secret into source control, a test, a fixture, a committed `.env`, a ticket, a PR body or
  a chat channel. Values go to the user, for the secret store or console.
- If a code change is needed (a callback host, a scope decision, `X-Business` support, refresh-token persistence or
  single-flight refresh), keep it secret-free and say what the human must set out of band.
- Close with: app name; the App Registry account that owns it; client ID; where the secret was delivered; the authorize,
  token and revoke endpoints and the API base; the callback URLs registered, exactly as typed; whether `readonly` or
  `full_access` was chosen and why; which countries are in scope and whether `country` is pinned; how a business is
  selected and whether `X-Business` is sent; the trial business used and whether its 12-month extension was requested;
  and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the client does not persist rotated refresh tokens, or refreshes
concurrently without a lock (report it — do not register and hope); the connector cannot send `X-Business` and the
customer has more than one business, or is an accountant (§8); the customer's Sage product turns out to be Intacct,
Sage 50/100/200/X3, Sage Active, or a South African / Australian / Asian regional Accounting product; the customer needs
Ireland or Canada and no trial business can be created there (Platform state); `full_access` is broader than the
customer's security review will accept and no narrower scope exists (§5); a Marketplace listing, the validation demo,
co-branding, data-hosting or security answers, or regional terms are in scope (§11); someone proposes rotating a live
client secret; or the portal does not match the **Platform state** section above.

## References

Verified 2026-09-20. **`developer.sage.com` returns HTTP 403 to non-browser clients** (Cloudflare bot protection), so
each documentation link below is given in its `.md` form, which Sage serves as "View as markdown" and which returns
HTTP 200 and plain text. **Drop the trailing `.md` to open the same page in a browser.** Every URL below was fetched and
returned HTTP 200 on that date.

- Authentication — authorize/token/revoke, parameters, scopes, token lifetimes, errors — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/authenticating/authentication.md
- High-level auth flow — identity hand-off, MFA, consent, region routing — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/authenticating/high-level-auth-flow.md
- OAuth troubleshooting — callback casing, country mismatch, refresh-token causes, concurrency — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/authenticating/oauth-troubleshooting.md
- Sign up for a developer account — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/getting-started/developer_signup.md
- Create your first app — fields, callback URL rules, client ID/secret, app image — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/getting-started/client_app_registration.md
- Quick start — https://developer.sage.com/accounting/docs/v1.0.0/guides/quick-start.md
- Getting started — the two accounts, trial businesses by region, variants, setup mistakes — https://developer.sage.com/accounting/docs/v1.0.0/guides/quick-start/getting-started.md
- Extend your trial to 12 months — https://developer.sage.com/accounting/docs/v1.0.0/guides/quick-start/extend-your-sage-business-cloud-accounting-trial.md
- Multi-business and business GUIDs — `X-Business`, lead business, businesses filters and pagination — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/key-concepts/multi-business.md
- Best practices — `X-Business` in production, token storage, per-user tokens, variants — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/key-concepts/best-practices.md
- API overview — products, versions, rate limits — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/key-concepts/overview.md
- Differences between Accounting and Start — per-variant endpoint availability — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/key-concepts/differences-between-accounting-and-start.md
- Partner Edition — accountant access and the lead-business problem — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/key-concepts/partner-edition.md
- Regional considerations — v3.1 country coverage, and how South Africa differs — https://developer.sage.com/accounting/docs/v1.0.0/guides/learning/regional-considerations.md
- Migrating from older API versions — https://developer.sage.com/accounting/docs/v1.0.0/guides/migrating/migrating-from-v3-to-v31.md
- Partner journey — build, validate, list, succeed — https://developer.sage.com/accounting/docs/v1.0.0/guides/journey.md
- Support — https://developer.sage.com/accounting/docs/v1.0.0/guides/support.md
- Support checklist — what Sage asks for on a case — https://developer.sage.com/accounting/docs/v1.0.0/guides/support/support-checklist.md
- App Registry (developer self-service) — https://developerselfservice.sageone.com/
- App Registry sign-in — https://developerselfservice.sageone.com/session/new
- Sage Accounting sign-in (the business account that authorizes) — https://app.sbc.sage.com/
- Authorization endpoint — https://www.sageone.com/oauth2/auth/central
- Sage service status — https://status.sage.com/
