Sage Business Cloud Accounting API: Capabilities, Rate Limits, and Integration Patterns

Type: ERP Integration System: Sage Business Cloud Accounting (API v3.1) Confidence: 0.88 Sources: 7 Verified: 2026-03-02 Freshness: 2026-03-02

TL;DR

System Profile

Sage Business Cloud Accounting (formerly Sage One) is Sage's cloud-native accounting platform targeting small and medium businesses. The API v3.1 is the current version and provides access to core accounting entities including contacts, invoices, bank accounts, ledger accounts, products, services, tax rates, payments, and journal entries. The API serves multiple countries (UK, Ireland, US, Canada, France, Spain, Germany, Australia, and others) with a multi-business routing model via the X-Business header. This card does NOT cover Sage Intacct (enterprise-grade, separate SOAP/REST API), Sage X3 (GraphQL API), Sage 50 (desktop SDK), or Sage 100 (on-premise).

PropertyValue
VendorSage
SystemSage Business Cloud Accounting
API SurfaceREST (JSON)
Current API Versionv3.1
Editions CoveredAll Sage Accounting cloud tiers
DeploymentCloud
API DocsSage Accounting Developer Portal
StatusGA

API Surfaces & Capabilities

Sage Business Cloud Accounting exposes a single REST API surface. There is no SOAP API, no Bulk API, and no native webhook/event-driven surface — polling is the only change detection mechanism.

API SurfaceProtocolBest ForMax Records/RequestRate LimitReal-time?Bulk?
REST API v3.1HTTPS/JSONIndividual CRUD, list queries, invoicing200 per page100/min per company, 2,500/day per companyYesNo
Swagger/OpenAPIJSON specAPI discovery, client generationN/AN/AN/AN/A

Key Endpoints

Endpoint CategoryExamplesOperations
Contacts/contacts, /contact_typesGET, POST, PUT, DELETE
Sales/sales_invoices, /sales_credit_notes, /sales_quotesGET, POST, PUT, DELETE
Purchases/purchase_invoices, /purchase_credit_notesGET, POST, PUT, DELETE
Banking/bank_accounts, /bank_transfersGET, POST, PUT, DELETE
Ledger/ledger_accounts, /journalsGET, POST, PUT, DELETE
Products & Services/products, /servicesGET, POST, PUT, DELETE
Tax/tax_rates, /tax_typesGET, POST, PUT, DELETE
Payments/contact_payments, /contact_allocationsGET, POST, PUT, DELETE
Settings/business_settings, /financial_settingsGET, PUT

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
Max records per page200List endpoints (GET)Default is 20; set via items_per_page parameter
Max request body size~1 MB (practical)POST/PUTSingle record per request — no batch endpoint
Data transfer limit100 MBPer appRolling 60-minute window
Max concurrent requests150Per app (all companies)Across all companies your app serves

Rolling / Daily Limits

Limit TypeValueWindowNotes
API requests per company2,50024h rollingPer-company, shared across ALL apps accessing the same company
API requests per minute1001 minutePer-company rate limit
API requests per app1,296,00024hApp-level aggregate across all companies
Failed login attempts201 hourIP blocked for 1 hour after exceeding

Authentication

Sage Business Cloud Accounting uses OAuth 2.0 authorization code flow exclusively. There is no client credentials (server-to-server) flow — every integration requires initial interactive user authorization.

FlowUse WhenToken LifetimeRefresh?Notes
OAuth 2.0 Authorization CodeAll integrations — user authorizes app accessAccess: 5 min; Refresh: 31 daysYes (rotating)Only available flow. User must interactively authorize. [src2, src5]

Token Lifecycle

  1. Authorization: Redirect user to Sage's OAuth endpoint with response_type=code
  2. Token exchange: POST to https://oauth.accounting.sage.com/token with grant_type=authorization_code
  3. Access: Include token as Authorization: Bearer {token} header. Token valid for 5 minutes.
  4. Refresh: POST with grant_type=refresh_token. Returns new access token AND new refresh token. Previous refresh token is invalidated.
  5. Business selection: Call GET /businesses to discover available businesses, then include X-Business: {business_id} header on all requests.

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START — User needs to integrate with Sage Business Cloud Accounting
|-- What's the integration pattern?
|   |-- Real-time (individual records, <1s)
|   |   |-- Data volume < 100 records/operation?
|   |   |   |-- YES --> REST API: one request per record
|   |   |   |-- NO --> REST API with rate limit throttling (max 100/min)
|   |   |-- Need change notifications?
|   |       |-- YES --> Poll with updated_from filter (no webhooks)
|   |       |-- NO --> Direct REST API calls
|   |-- Batch/Bulk (scheduled, high volume)
|   |   |-- Data volume < 2,500 records/day?
|   |   |   |-- YES --> REST API with sequential requests + rate limiting
|   |   |   |-- NO --> STOP — 2,500/day limit cannot be raised
|   |   |       |-- Consider Sage Intacct (100K+ transactions/month)
|   |-- Event-driven
|   |   |-- Native webhooks? NO — Sage Accounting has no webhooks
|   |   |-- Alternative --> Poll with updated_from/updated_to timestamps
|   |-- File-based (CSV/XML)
|       |-- NOT SUPPORTED via API — use Sage's built-in CSV import UI
|-- Which direction?
|   |-- Inbound --> max 2,500 writes/day per company
|   |-- Outbound --> max 2,500 reads/day per company
|   |-- Bidirectional --> budget 1,250 each direction per day
|-- Token management
    |-- Access token expires every 5 minutes
    |-- Refresh proactively before each batch
    |-- Serialize refresh token operations (no parallel refresh)

Quick Reference

OperationMethodEndpointPayloadNotes
List contactsGET/contacts?items_per_page=200N/APaginated; use page parameter
Create contactPOST/contactsJSONSingle record per request
Get sales invoiceGET/sales_invoices/{id}N/AReturns full invoice with line items
Create sales invoicePOST/sales_invoicesJSONInclude invoice_lines array
List bank accountsGET/bank_accountsN/AReturns all bank accounts
Create journalPOST/journalsJSONDouble-entry: debit + credit lines required
List ledger accountsGET/ledger_accountsN/AChart of accounts
Get businessesGET/businessesN/AReturns businesses for authenticated user
List tax ratesGET/tax_ratesN/ACountry-specific tax rates
Get financial settingsGET/financial_settingsN/AFiscal year, base currency, etc.

Step-by-Step Integration Guide

1. Register your application

Register at the Sage Developer Portal to obtain a Client ID and Client Secret. Specify your redirect URI exactly. [src2]

# Record these values after registration:
# - Client ID: {your-client-id}
# - Client Secret: {your-client-secret}
# - Redirect URI: https://yourapp.example.com/sage/callback
# - Subscription Key: {your-subscription-key}

Verify: Your application appears in the Sage Developer Portal dashboard with an active Client ID.

2. Authorize the user via OAuth 2.0

Redirect the user to Sage's authorization endpoint. After the user grants permission, Sage redirects back with an authorization code. [src2]

import urllib.parse

client_id = "YOUR_CLIENT_ID"
redirect_uri = "https://yourapp.example.com/sage/callback"

auth_url = (
    "https://www.sageone.com/oauth2/auth/central"
    f"?response_type=code"
    f"&client_id={client_id}"
    f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
    f"&scope=full_access"
)
print(f"Redirect user to: {auth_url}")

Verify: User is redirected back to your redirect URI with ?code={authorization_code} in the query string.

3. Exchange authorization code for tokens

POST the authorization code to the token endpoint to receive an access token and refresh token. [src2, src5]

import requests

token_url = "https://oauth.accounting.sage.com/token"

response = requests.post(token_url, data={
    "grant_type": "authorization_code",
    "code": authorization_code,
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "redirect_uri": "https://yourapp.example.com/sage/callback"
})

tokens = response.json()
access_token = tokens["access_token"]       # Valid for ~5 minutes
refresh_token = tokens["refresh_token"]     # Valid for 31 days, rotates on use

Verify: Response contains access_token, refresh_token, and resource_owner_id fields.

4. Discover and select business

After authentication, discover available businesses and store the selected business ID. [src5, src7]

base_url = "https://api.accounting.sage.com/v3.1"
headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json"
}

response = requests.get(f"{base_url}/businesses", headers=headers)
businesses = response.json()
selected_business_id = businesses["$items"][0]["id"]

Verify: GET /businesses returns at least one business with id, name, and country fields.

5. Make API calls with proper headers

Include the access token and business ID in all subsequent API requests. [src1, src5]

headers = {
    "Authorization": f"Bearer {access_token}",
    "X-Business": selected_business_id,
    "Content-Type": "application/json",
    "Accept": "application/json"
}

response = requests.get(
    f"{base_url}/contacts?items_per_page=200&page=1",
    headers=headers
)

data = response.json()
contacts = data["$items"]
total = data["$total"]

Verify: Response contains $items array with contact records and $total count.

6. Implement token refresh

Proactively refresh the access token before it expires (every 5 minutes). Store the new refresh token immediately. [src5, src7]

def refresh_access_token(current_refresh_token):
    response = requests.post(token_url, data={
        "grant_type": "refresh_token",
        "refresh_token": current_refresh_token,
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET"
    })

    if response.status_code == 200:
        tokens = response.json()
        # CRITICAL: Store the new refresh token immediately
        # The old refresh token is now permanently invalid
        new_access_token = tokens["access_token"]
        new_refresh_token = tokens["refresh_token"]
        return new_access_token, new_refresh_token
    else:
        raise Exception(f"Token refresh failed: {response.json()}")

Verify: New access token works with GET /businesses. New refresh token is different from the previous one.

Code Examples

Python: List all sales invoices with pagination

# Input:  access_token, business_id, base_url
# Output: All sales invoices across all pages

import requests
import time

def get_all_invoices(access_token, business_id, base_url):
    headers = {
        "Authorization": f"Bearer {access_token}",
        "X-Business": business_id,
        "Accept": "application/json"
    }
    all_invoices = []
    page = 1

    while True:
        response = requests.get(
            f"{base_url}/sales_invoices",
            headers=headers,
            params={"items_per_page": 200, "page": page}
        )
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
        data = response.json()
        all_invoices.extend(data.get("$items", []))
        if data.get("$next"):
            page += 1
            time.sleep(0.6)  # Respect 100/min rate limit
        else:
            break
    return all_invoices

JavaScript/Node.js: Create a sales invoice

// Input:  accessToken, businessId, invoice data
// Output: Created invoice object with Sage-assigned ID

const fetch = require("node-fetch");
const BASE_URL = "https://api.accounting.sage.com/v3.1";

async function createSalesInvoice(accessToken, businessId, invoiceData) {
  const response = await fetch(`${BASE_URL}/sales_invoices`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "X-Business": businessId,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      sales_invoice: {
        contact_id: invoiceData.contactId,
        date: invoiceData.date,
        due_date: invoiceData.dueDate,
        invoice_lines: invoiceData.lines.map((line) => ({
          description: line.description,
          ledger_account_id: line.ledgerAccountId,
          quantity: line.quantity,
          unit_price: line.unitPrice,
          tax_rate_id: line.taxRateId,
        })),
      },
    }),
  });
  if (!response.ok) throw new Error(`Sage API error: ${response.status}`);
  return response.json();
}

cURL: Quick API test

# Input:  access_token, business_id
# Output: List of contacts from Sage Accounting

# Get businesses
curl -s "https://api.accounting.sage.com/v3.1/businesses" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json" | jq '.["$items"][] | {id, name}'

# List contacts
curl -s "https://api.accounting.sage.com/v3.1/contacts?items_per_page=10" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-Business: YOUR_BUSINESS_ID" \
  -H "Accept: application/json" | jq '.["$items"][] | {id, name}'

Data Mapping

Field Mapping Reference

Sage API FieldCommon ERP EquivalentTypeTransformGotcha
contact.nameCustomer/Vendor nameString (max 200)DirectMust be unique per contact type
sales_invoice.dateInvoice dateString (ISO 8601)YYYY-MM-DD formatNo time component — date only
sales_invoice.total_amountInvoice totalDecimalRead-onlyComputed from line items — cannot set directly
ledger_account.nominal_codeAccount code/numberStringCountry-specific formatUK uses 4-digit codes; US differs
tax_rate.percentageTax rateDecimalDirectCountry-specific; UK VAT vs US sales tax
bank_account.balanceBank balanceDecimalRead-onlyUpdated by bank transactions only
contact.currency_idDefault currencyString (ISO 4217)GBP, USD, EURSet at creation; changing requires care
payment.amountPayment amountDecimalDirectMust match allocated invoice amounts

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeMeaningCauseResolution
401UnauthorizedAccess token expired (>5 min) or invalidRefresh the access token and retry
403ForbiddenInsufficient permissions or wrong businessVerify X-Business header and user permissions
404Not FoundRecord ID does not exist or wrong API versionCheck endpoint path includes /v3.1/ and record ID is valid
409ConflictDuplicate record or concurrent modificationCheck for existing record; implement optimistic locking
422Unprocessable EntityValidation error — missing/invalid fieldsRead error message body for specific field errors
429Too Many RequestsRate limit exceeded (100/min or 2,500/day)Implement exponential backoff; respect Retry-After header
500Internal Server ErrorSage platform issueRetry with backoff; report to Sage if persistent

Failure Points in Production

Anti-Patterns

Wrong: Caching access tokens for 1 hour

# BAD — Sage v3.1 tokens expire in 5 minutes, not 1 hour
token = get_token()
cache.set("sage_token", token, ttl=3600)  # 1 hour cache — WRONG

Correct: Refresh proactively before each batch

# GOOD — check token age, refresh if >4 minutes old
import time

token_acquired_at = time.time()

def get_valid_token():
    global token_acquired_at, access_token, refresh_token
    if time.time() - token_acquired_at > 240:  # 4 min threshold
        access_token, refresh_token = refresh_access_token(refresh_token)
        token_acquired_at = time.time()
    return access_token

Wrong: Parallel refresh token operations

# BAD — two threads use the SAME refresh token — race condition
import threading

def worker(refresh_token):
    new_access = refresh_access_token(refresh_token)  # Race!
    make_api_calls(new_access)

t1 = threading.Thread(target=worker, args=(current_refresh,))
t2 = threading.Thread(target=worker, args=(current_refresh,))
t1.start(); t2.start()

Correct: Serialize refresh operations with a lock

# GOOD — use a mutex to ensure only one refresh at a time
import threading

token_lock = threading.Lock()

def get_fresh_token():
    with token_lock:
        if needs_refresh():
            return refresh_access_token(current_refresh_token)
        return current_access_token

Wrong: Fetching all records to detect changes

# BAD — downloads ALL contacts every sync, wastes API quota
all_contacts = get_all_contacts()  # Could be 500+ API calls
for contact in all_contacts:
    if contact["updated_at"] > last_sync:
        process(contact)

Correct: Use updated_from filter for incremental sync

# GOOD — only fetch records changed since last sync
changed_contacts = requests.get(
    f"{base_url}/contacts",
    headers=headers,
    params={
        "updated_from": last_sync_timestamp,
        "items_per_page": 200
    }
)

Common Pitfalls

Diagnostic Commands

# Test authentication — exchange code for tokens
curl -s -X POST "https://oauth.accounting.sage.com/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=YOUR_AUTH_CODE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=YOUR_REDIRECT_URI" | jq .

# Refresh access token
curl -s -X POST "https://oauth.accounting.sage.com/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET" | jq .

# List available businesses
curl -s "https://api.accounting.sage.com/v3.1/businesses" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" | jq '.["$items"][] | {id, name, country}'

# Check API connectivity
curl -s "https://api.accounting.sage.com/v3.1/me" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-Business: YOUR_BUSINESS_ID" | jq .

# List ledger accounts
curl -s "https://api.accounting.sage.com/v3.1/ledger_accounts?items_per_page=5" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-Business: YOUR_BUSINESS_ID" | jq '.["$items"][] | {id, nominal_code, name}'

Version History & Compatibility

API VersionStatusKey ChangesMigration Notes
v3.1Current (GA)5-min access tokens, rotating refresh tokens, X-Business header replaces X-Site, unified multi-country supportMust update token management logic; header name change; refresh token rotation requires serialized storage
v3Deprecated60-min access tokens, non-rotating refresh tokens, X-Site headerPrevious stable version — sunset timeline not publicly announced
v2EOLLegacy authentication modelFully decommissioned; no migration path except full rewrite to v3.1
v1EOLOriginal Sage One APIFully decommissioned

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
SMB accounting integration with <2,500 ops/dayHigh-volume enterprise integration (>2,500 records/day)Sage Intacct (100K+ API transactions/month)
Building SaaS integration with Sage Accounting customersNeed S2S daemon/batch without user involvementSage Intacct or Xero API (supports client credentials)
Multi-country accounting (UK, US, CA, FR, ES, DE, AU)Need real-time event notifications (webhooks)Xero API (native webhooks) or QuickBooks API
Low-volume sync of accounting dataNeed bulk data migration (>10K records)Manual Sage CSV import or Sage Intacct
Read-only reporting and dashboardsComplex multi-entity financial consolidationSage Intacct (multi-entity natively)

Important Caveats

Related Units