Sager for Craft CMS

Troubleshooting

Start with the overview and the log

Sager → Overview has a Setup checklist. If anything on it isn't ticked, that's the first thing to fix. From the console, php craft sager/connect/status shows the same thing.

Then open the failing order in Sager → Orders. It shows the last error and links to the order's own log entries. Each log entry has the full request Sager sent and the full response Sage gave, with credentials and tokens redacted. Sage's own error message is in the entry's Message, and its error code under Sage data code.

From the console:

php craft sager/log/failures
php craft sager/sync/preview AB-1234
StatusMeaning
400 on oauth.refresh_tokenSage refused the refresh token. The connection is marked expired. See Reconnect Sager to Sage
401The access token expired mid-request. Sager refreshes and retries once on its own. Only a problem if it repeats
403Sage refused access. Usually the Sage user who connected can't do this in this business
404 on get v3.1/businessesNormal. See No businesses listed
422Sage rejected the data. The message says which field. See Sage rejects the invoice
429Rate limited. Sager waits as long as Sage asks and retries. Nothing to do
5xx, or no statusSage or the network failed. Sager retries

Connecting

"Add your Sage client ID and secret first"

Connect to Sage needs a client ID and secret saved in settings, and both environment variables set on this environment. If the fields show $SAGE_CLIENT_ID but the variable is missing from this server's .env, it counts as empty.

The client secret won't save

The client secret has to be an environment variable reference, such as $SAGE_CLIENT_SECRET. Settings are written to project config, and Sager refuses to put a literal secret there. Add the secret to .env and reference it. See Configuration.

Sage says the redirect URI is invalid

The callback URL registered with your Sage app has to match Redirect URI on the settings screen exactly: http or https, www or not, the host, and whether the URL contains index.php?p=. Copy it from the settings screen on the environment you're connecting from. Each environment needs its own callback URL registered with the app.

If the Redirect URI shows the wrong host, for example an internal one behind a proxy, set Redirect URI override to the public URL of actions/sager/connect/callback and register that.

"That Sage response did not match the request Sager sent"

Sager checks that the response from Sage belongs to the connection attempt your browser started. This fails when:

  • the connection was started in another tab, or an earlier attempt was retried
  • the CP and the redirect URI are on different hosts, so the browser's Craft session isn't sent back with Sage's response

Press Connect to Sage again, from a CP URL on the same host as the redirect URI.

"Sage refused the connection"

Sage's own reason follows the message. Usually the approval was cancelled, or the app's client ID is wrong for this environment.

No businesses listed

The Sage business dropdown is filled from Sage's list of businesses for your login. Some logins get a 404 from that list, and a login that reaches only one business often gets an empty one. Both are shown as Sage listed no businesses for this connection.

In that case Sage posts to the login's default business. Check which one with php craft sager/connect/businesses, or in Sage itself. If the overview's Sage business chosen step stays unticked, connect with a Sage login that can see its businesses.

Reconnect Sager to Sage

Sage access tokens last five minutes. Refresh tokens last 31 days, and are replaced every time they're used. Sager stores each new one before using it, and only one process refreshes at a time. Even so, a refresh token can be lost, and when it is there's no way to get it back. Sager marks the connection expired and stops trying. Orders queued after that fail with The Sage connection has expired. Reconnect Sager to Sage.

To fix it, press Reconnect to Sage on the settings screen, approve, and choose the business again. This works on production. Then send the orders that failed: filter Sager → Orders to Failed, or run php craft sager/sync/pending.

Common causes:

CauseWhat happened
No syncing for 31 daysThe refresh token expired unused. Authorisation expires on the overview shows the deadline. It moves forward every time Sager talks to Sage
A database restored from backupThe backup holds an old refresh token that Sage has since replaced
The same database on two environmentsBoth use the same refresh token. The second to refresh is refused. See Configuration
Craft's security key changedThe stored tokens can't be decrypted. The message is Sager could not read the stored Sage tokens
Access revoked in SageSomeone removed the app's access, or the Sage user who connected lost access to the business

Settings and mapping are read-only on production

That's expected. Settings and mapping are project config. Where allowAdminChanges is off, the screens show what's deployed but can't save. Change them on your development site and deploy.

What does work on production: connecting, reconnecting, disconnecting, choosing the business, Reload from Sage, and clearing the log. These are stored in the database, not project config.

The mapping doesn't match production

Every invoice fails on production with a 422 about a ledger account or tax rate that doesn't exist, but the same order works on dev.

The mapping is project config. Its values are GUIDs from the Sage business your development site was connected to when you saved it. If production is connected to a different business, none of them exist there.

Check both environments with php craft sager/connect/status and compare the business. Then either:

  • connect production to the same business dev used, or
  • connect dev to production's business, redo the mapping, and deploy it.

The same happens after choosing a different business on an environment that's already mapped. Changing the business doesn't clear the mapping. See Configuration.

The mapping screen is empty or out of date

  • "Connect Sager to Sage first": the mapping screen is built from your Sage business, so it needs a live connection on this environment.
  • "Sager could not read your Sage lists": the message is Sage's. Usually the connection has expired, or no business is chosen.
  • A new account or tax rate is missing: the lists are cached for an hour. Press Reload from Sage, or run php craft sager/connect/refresh.
  • A ledger account is missing: the dropdowns only offer accounts Sage makes available for sales. Check the account's settings in Sage.

Orders aren't being sent

Nothing has happened to the order: no status, no log entries.

  1. Is Sager connected? Orders completed while disconnected aren't queued, even after you connect. Send them with Queue everything not sent.
  2. Is Sync automatically on? If it's off, or the trigger is Never — manual only, nothing is sent on its own.
  3. Does the order meet the trigger? With When the order is paid, an unpaid order waits. With When the order reaches a status, the order has to move into a ticked status. An order that was already in it when you changed the setting isn't picked up.
  4. Is the queue running? Sager does all its work in Craft's queue. Check Utilities → Queue Manager for waiting Syncing an order to Sage jobs. If runQueueAutomatically is off, you need a queue worker or a cron job running php craft queue/run.

"Skipped"

The order didn't qualify. The reason is on the order's Sager page:

ReasonWhat to do
The order total is zero.Turn off Skip zero-total orders, or send it by hand
The order is not paid, and Sager is set to sync on payment.Wait for payment, or send it by hand
The order status is not one Sager syncs on.Tick its status under Statuses, or send it by hand
The order is not complete.Carts are never sent

Sending by hand bypasses the trigger and zero-total rules, but only for completed orders.

Sage rejects the invoice (422)

Sage's message names the field. The usual causes:

Message mentionsCause
ledger_account_idThe sales ledger account, or a product type's account, is unmapped, or belongs to another business
tax_rate_idA tax rate is unmapped, belongs to another business, or isn't valid in the business's country
contact_idThe contact Sager linked to this customer was deleted in Sage
currency_idThe order's currency isn't enabled in the Sage business
bank_account_id, transaction_type_idSee Payments aren't sent

A 422 isn't retried, because Sage would give the same answer. Fix the mapping, deploy it, then send the order again.

Errors Sager raises before sending

Some problems are caught while building the invoice, so nothing is sent to Sage. They show on the order and in the preview:

  • "No sales ledger account is mapped." Map one on Sager → Mapping.
  • "No Sage tax rate is mapped for the X tax category, used by Y." Map that tax category, or a Default tax rate.
  • "The order has shipping tax but no shipping tax rate is mapped." Map a Shipping tax rate or a Default tax rate.
  • "The order has no lines to invoice." The order has no line items.

"The rebuilt invoice is X away from the order total"

The invoice Sager built doesn't add up to the order's total in Commerce, by more than the rounding tolerance. Sager won't post numbers it can't explain.

Run php craft sager/sync/preview AB-1234, or press Preview payload. The notes say what Sager did with each adjustment. The usual causes:

  • Order-level tax with no shipping. Sage only holds order-level tax on the shipping fields. A tax that isn't attached to a line, on an order with no shipping, has nowhere to go. Attach the tax to line items in your Commerce tax rates.
  • An adjustment from another plugin that changes the total in a way Sager can't see. Each unrecognised order-level adjustment gets its own line, so if one is missing, check whether it's marked included.
  • Prices with more decimal places than the currency. Usually within tolerance. If not, raise Rounding tolerance a little.

The same customer appears twice in Sage

Sager only reuses an existing Sage contact when it can be sure:

  • the contact has Sager's reference for this customer, such as WEB-12, or
  • exactly one customer contact in Sage has the order's email address

Two Sage contacts with the same email means Sager can't be sure, and creates a new contact with its own reference. That's the one Sager has linked to the customer, so keep it when you tidy up in Sage.

Changing Contact reference prefix after going live means contacts created earlier no longer match by reference. Pick it once.

Payments aren't sent

MessageFix
No Sage bank account is mappedMap a Bank account
No Sage transaction type is mapped for customer receiptsSager couldn't find a receipt type in your business. Map Receipt transaction type
Invoice is already fully allocated (log, payment.skipped)Payments already sent cover the invoice. Nothing more is allocated. Expected after an overpayment

Only successful purchases and captures are sent. An authorised payment that hasn't been captured isn't money yet. Once it's captured, it's sent.

After fixing the mapping, press Re-check on the order. It sends any payments Sage hasn't had, without touching the invoice.

A refund didn't reach Sage

  • Send refunds and Sync automatically both have to be on when the refund happens. A refund made while either was off isn't sent later.
  • A refund made before the order's invoice exists waits. Sager sends the invoice first, then the credit note. Check the order's status.
  • A refund that Sage rejected shows in Sager → Log as a failed post v3.1/sales_credit_notes entry, with Sage's reason.

Invoices arrive as drafts

Sage creates invoices as drafts unless told otherwise. Turn on Release invoices to release each one as soon as it's created. If a release fails, the invoice stays a draft, and the log shows an invoice.release entry. Release it in Sage.

The log is growing

The log is pruned automatically during Craft's garbage collection, which runs on a share of web requests by default. Entries older than Log retention days are deleted, and 0 keeps everything. If you've turned garbage collection off (gcProbability set to 0), run php craft sager/log/prune or php craft gc from cron.

Getting help

Email justin@justinholt.com. Include the output of php craft sager/connect/status and php craft sager/log/failures, and for an invoice problem, php craft sager/sync/preview <number> for one affected order. Remove customer details before you send it.