Configuration
How Sager stores its configuration
Sager keeps two kinds of configuration, in two different places. Most of what follows comes back to this.
| What | Where it lives | Changed on production? |
|---|---|---|
| Settings: credentials (as env var references), triggers, references, retries, retention | Project config | No. Change them on dev and deploy |
| Mapping: the Sage ledger accounts, tax rates, bank account and so on | Project config | No. Change them on dev and deploy |
| Connection: OAuth tokens, the chosen Sage business | Database, encrypted | Yes, on every environment |
| Sage lists: the cached chart of accounts, tax rates, etc. | Craft's cache | Yes |
So on production, where allowAdminChanges is off, admins can still connect to Sage, choose the
business, reload the Sage lists and clear the log. The Settings and Mapping screens still
appear, read-only, with a note saying so. The Sage connection pane on the settings screen keeps
working.
Creating the Sage app
Sager connects through OAuth 2.0, using an app you register with Sage.
- Sign in to Sage's developer portal at developer.sage.com and create an app for Sage Business Cloud Accounting.
- In Craft, open Sager → Settings and copy the Redirect URI. It's Craft's action URL for
sager/connect/callback, so it looks likehttps://example.com/actions/sager/connect/callbackorhttps://example.com/index.php?p=actions/sager/connect/callback, depending on youromitScriptNameInUrlssetting. - Register that exact URL as the app's callback URL. Scheme, host, path and trailing characters all have to match. Add one callback URL per environment you'll connect from.
- Copy the app's client ID and client secret.
Credentials
Put both values in .env:
# .env
SAGE_CLIENT_ID="…"
SAGE_CLIENT_SECRET="…"
Then type $SAGE_CLIENT_ID and $SAGE_CLIENT_SECRET into the Client ID and Client secret
fields. They autosuggest.
The client secret must be an environment variable reference. Settings are written to project
config, and project config is committed. Sager refuses to save a literal secret, so it never lands
in config/project/*.yaml or your git history. The client ID isn't a secret, and can be either a
literal or an environment variable.
Connecting
- Save the credentials.
- Press Connect to Sage. You're sent to Sage to sign in and approve the app.
- Sage sends you back to the redirect URI, and Sager stores the tokens, encrypted with Craft's security key.
- If your Sage login reaches one business, Sager selects it. If it reaches more, choose one from Sage business and press Use this business.
- You land on Sager → Mapping.
The Country setting is optional. A two-letter code such as GB or US sends you straight to
the right Sage region instead of Sage's region picker.
Connecting again, after a disconnect or an expiry, forgets the business that was chosen before. Pick it again afterwards.
Disconnecting
Disconnect throws away the stored tokens and the chosen business on this environment. It doesn't revoke the app in Sage. Orders stop being sent until you connect again. Orders completed while disconnected aren't queued, so send them afterwards with Queue everything not sent.
Choosing a different business
Changing the business clears the cached Sage lists, because ledger accounts and tax rates belong to a business. It doesn't clear the mapping. The GUIDs saved there belong to the old business, and Sage rejects them. Redo the mapping on your development site, against the new business, and deploy it.
Settings
Sager → Settings, or Settings → Plugins → Sager. Admins only. Sections are listed as they appear on the screen.
Sage connection
| Setting | Config key | Default | What it does |
|---|---|---|---|
| Client ID | clientId | blank | Your Sage app's client ID. A literal or an environment variable |
| Client secret | clientSecret | blank | Your Sage app's client secret. Must be an environment variable reference, such as $SAGE_CLIENT_SECRET |
| Redirect URI | — | — | Read-only. The URL to register with your Sage app |
| Redirect URI override | redirectUriOverride | blank | Only for when Craft can't work out its own public URL, such as behind some proxies. Accepts an environment variable |
| Country | countryCode | blank | Optional two-letter code for the Sage region, such as GB or US |
| Status | — | — | Connected, expired or not connected, with Connect, Reconnect or Disconnect |
| Sage business | — | — | Which business this store posts to. Stored in the database, not project config |
When to sync
| Setting | Config key | Default | What it does |
|---|---|---|---|
| Sync automatically | autoSync | on | Turn off to send nothing on its own. Orders, payments and refunds are then only sent from the CP or the console |
| Trigger | syncTrigger | When the order is completed | complete, paid, status or manual. See Triggers |
| Statuses | syncStatusHandles | none | Order status handles, used when the trigger is status |
| Send payments | syncPayments | on | Captured payments become Sage contact payments allocated against the invoice |
| Send refunds | syncRefunds | on | Refunds become Sage sales credit notes |
| Release invoices | releaseInvoices | off | Asks Sage to take each invoice out of draft as soon as it's created |
| Skip zero-total orders | skipZeroTotalOrders | on | Free and fully comped orders aren't sent |
Documents
| Setting | Config key | Default | What it does |
|---|---|---|---|
| Invoice reference | invoiceReferenceFormat | {reference} | The Sage invoice reference. {number}, {shortNumber}, {reference} and {id} are replaced. Up to 60 characters. An order with no reference uses its short number |
| Contact reference prefix | contactReferencePrefix | WEB | Stamped on contacts Sager creates, such as WEB-12, and used to find them again. Up to 20 characters |
| Copy the customer note | copyCustomerNote | on | Puts the order's customer note in the invoice's notes |
| Rounding tolerance | roundingToleranceMinor | 5 | In minor units: cents, pence. See Usage |
Retries and logging
| Setting | Config key | Default | What it does |
|---|---|---|---|
| Maximum attempts | maxAttempts | 5 | How many tries an order gets before it's marked failed, 1–50 |
| First retry delay | retryDelaySeconds | 60 | Seconds before the first retry. Doubles each attempt, up to an hour, unless Sage sends its own Retry-After |
| Log retention | logRetentionDays | 30 | Days of connection log to keep. 0 keeps everything |
Retention is enforced during Craft's garbage collection, so you don't need a cron job for it.
php craft sager/log/prune runs the same pruning on demand.
Triggers
| Trigger | An order is queued when |
|---|---|
| When the order is completed | the customer completes checkout |
| When the order is paid | Commerce marks the order paid. A completed order that isn't paid yet is skipped |
| When the order reaches a status | the order moves into one of the ticked Statuses |
| Never — manual only | never. Send orders from the CP or the console |
Payments and refunds follow on their own whatever the trigger, as long as Sync automatically is on. A payment captured before the invoice exists is sent with the invoice.
Mapping
Sager → Mapping. Admins only. Every field is a dropdown built from your own Sage business, so you never type a GUID. The screen needs a live connection to read those lists.
Sales
| Field | Config key | What it's for |
|---|---|---|
| Sales ledger account | salesLedgerAccountId | Where product revenue lands. Required. Nothing is sent without it |
| Default tax rate | defaultTaxRateId | The tax rate for any line with nothing more specific. Every Sage line needs one |
| Zero-rated tax rate | zeroTaxRateId | Used for lines Commerce didn't tax, and for discount and rounding lines. Without it, those lines use the default rate, and Sage may add tax to a line that never had any |
Shipping, discounts and rounding
| Field | Config key | What it's for |
|---|---|---|
| Shipping tax rate | shippingTaxRateId | The tax rate on the invoice's shipping. Falls back to the default rate |
| Discount ledger account | discountLedgerAccountId | Where the order-level discount line posts. Falls back to the sales ledger account |
| Rounding ledger account | roundingLedgerAccountId | Where a rounding line posts. Falls back to the sales ledger account |
Payments
| Field | Config key | What it's for |
|---|---|---|
| Bank account | bankAccountId | Where received money lands. Required when Send payments is on |
| Payment method | paymentMethodId | Optional. The Sage payment method on each payment |
| Receipt transaction type | receiptTransactionTypeId | Usually Customer Receipt. Leave it unmapped and Sager picks the receipt type from your Sage business |
Tax categories
One dropdown per Commerce tax category (config key taxCategoryMap, category ID → Sage tax rate
ID). A line uses the rate mapped to its tax category. An unmapped category uses the zero-rated rate
if Commerce charged no tax on the line, and the default rate otherwise.
Map these if you sell at more than one rate, for example standard-rated goods and zero-rated books.
Product types
Optional. One dropdown per Commerce product type (config key productTypeMap, product type ID →
Sage ledger account ID). A line for a product of that type posts to that ledger account instead of
the sales ledger account. Useful when goods and services are reported separately.
Sage lists
The dropdowns are cached for an hour. After changing your chart of accounts or tax rates in Sage,
press Reload from Sage, or run php craft sager/connect/refresh. Both work on production.
The ledger account dropdowns show the accounts Sage makes available for sales.
Development and production
The mapping is project config, and its values are GUIDs from one specific Sage business. Your development site and your production site therefore have to connect to the same Sage business. If they're connected to different businesses, such as a Sage trial business on dev and the real one on production, every GUID in the deployed mapping is wrong on production, and Sage rejects every invoice.
Each environment makes its own connection. Tokens are database state and never deploy. A sensible setup:
- On dev, connect to the production Sage business and build the mapping. Reading the lists changes nothing in Sage.
- Keep dev from posting test orders into your real books: turn Sync automatically off for dev
in
config/sager.php(below), or press Disconnect once the mapping is saved. - Deploy. On production, press Connect to Sage, approve, and choose the same business.
- Check Sager → Overview on production.
If you really do need a different business on another environment, override the mapping fields for
that environment in config/sager.php with environment variables. Otherwise, redo the mapping.
Copying a database between environments
Sage refresh tokens are replaced every time they're used. If you copy the production database to dev and both environments then use the same stored connection, whichever refreshes second is refused, and has to be reconnected by hand. That can be production.
After pulling a production database down, press Disconnect on dev straight away, before its queue runs, then reconnect dev if you need it.
The config file
Any setting can be fixed in config/sager.php, using the config keys above. Values in the file
override whatever is saved in project config. That's Craft's standard behaviour for plugin
settings. A field set this way still shows on the settings screen, but saving the screen doesn't
change it.
<?php
use craft\helpers\App;
return [
'clientId' => '$SAGE_CLIENT_ID',
'clientSecret' => '$SAGE_CLIENT_SECRET',
// Off on dev, so test orders don't land in the real books.
'autoSync' => App::env('CRAFT_ENVIRONMENT') === 'production',
'invoiceReferenceFormat' => 'WEB-{reference}',
'logRetentionDays' => 14,
];
Permissions
| Permission | What it allows |
|---|---|
| View Sage sync status | Sager → Overview, Sager → Orders, each order's Sager page, and the Sage panel on Commerce's order screen |
| ↳ Send orders to Sage | Send, Re-check, Send to Sage now, Queue everything not sent and Preview payload |
| View the connection log | Sager → Log and its entries, read-only |
Every Sager screen also requires Commerce's Manage orders permission, because the order screens, previews and log all show customer names, addresses and order totals.
Admin-only, whatever the permissions:
- Sager → Settings and Sager → Mapping. Saving either also needs
allowAdminChanges. - Connecting, disconnecting and choosing the business. These work wherever admin changes are off.
- Clear the log.