Ledgr/Documentation/Connecting Aura POS

Connecting Aura POS

Aura is the till. Ledgr is the books. This is how a day's takings get from one to the other without anyone retyping them.

Plan.

Needs Reports for the ledger and an API key. API keys are included from Business, and the pricing page shows how many each plan holds. Aura is a separate product with its own subscription.

What Aura is

Aura POS is a hospitality point-of-sale system for South African restaurants — on Android, iOS, desktop and the web, with its own server behind the tills. Table-side QR ordering, a kitchen display, split bills, tips, Yoco/SnapScan/Zapper, WhatsApp receipts, and offline-first sync built for load shedding.

It is not a Ledgr module and it is not becoming one. A restaurant till has a different shape from an accounting ledger: it cares about covers, courses, prep stations and shift hand-overs, and it has to keep taking orders when the power and the network are both gone. Building that into an accounting package would compromise both. What the two share is the money.

Which way the data flows

One direction only: Aura → Ledgr. Aura owns the sale, the menu, the stock behind the menu and the payment. Ledgr owns the books, the VAT return and the financial statements. Ledgr never writes back into the till, because a POS that could have its takings edited from an accounting package is a POS whose cash-up means nothing.

Push a day, not a sale

Aura already computes double-entry journals for every checkout, in its own ledger. The integration sends those to Ledgr — summarised by day, per location, not one document per sale.

This matters more than it sounds. A restaurant doing 300 covers a night would create 300 Ledgr documents an evening, 9 000 a month, for a set of books whose owner wants one line saying what Tuesday took. The detail belongs in Aura, which is where anyone asking "what did table twelve order" is going to look anyway. Ledgr gets the total, the VAT, and a reference back to the day.

Push after the cash-up, not during service. A day that is still open can still change — a void, a late tip adjustment, a re-opened tab — and a journal posted mid-service has to be reversed rather than simply not yet written.

Authenticating

Aura holds a Ledgr API key and exchanges it for a short-lived token, exactly as any other integration does. It never needs a Ledgr password.

  1. In Ledgr: create the key

    Roles & Users → API Keys → Generate. Give it reports:write — posting a journal needs create permission on the ledger — and nothing else. Looking up the company its journals belong to (GET /api/companies) is a read every key may make, whatever its scopes. Name it "Aura POS" so it can be revoked without anyone wondering what it was for.

  2. In Aura: store the key

    As a server-side secret, not in a client build. The POS terminals never talk to Ledgr; only Aura's backend does.

  3. Exchange it for a token

    POST /auth/token with the key as a bearer credential returns a one-hour access token. Aura's nightly job asks for a fresh one each run rather than caching it — a job that runs once a day gains nothing from reusing an hour-old token.

  4. Find the company

    GET /api/companies with that token lists the companies the key may reach. Every ledger call then carries the company's id in an X-Company-Id header — there is no default company, and a call without one is refused. The id never changes, so look it up once and keep it with the key.

Full details of the exchange, the scopes and the rate limits are in the developer documentation.

Mapping the accounts — read this part twice

Aura's journal generator uses its own account numbers, and they are not Ledgr's. The two charts overlap in the ranges but disagree on the codes, and two of the collisions are the dangerous kind: Aura's 2000 is VAT Output, while Ledgr's 2000 is the Non-Current Liabilities header. Aura's 2100 is Tips Payable, while Ledgr's 2100 is the Current Liabilities header.

Posting Aura's codes into Ledgr unchanged would credit header accounts.

The trial balance would still balance — debits would equal credits — and the balance sheet would be nonsense, with VAT owed to SARS sitting inside a heading. That is the worst class of accounting bug, because nothing looks broken. Map every line explicitly, and let an unmapped code fail loudly rather than defaulting to anything.

AuraWhat it isLedgrLedgr account
1000Cash1360Petty Cash
1010Yoco settlement due1330Other Receivables & Prepayments
1100Inventory1310Inventory / Stock on Hand
2000VAT Output ⚠2130VAT Output (Control)
2100Tips Payable ⚠2120Other Payables & Accruals
2200Processor fees payable2120Other Payables & Accruals
4000/4010Revenue4010Sales — Goods
5000Cost of goods sold5040Cost of Goods Sold
6000Yoco fees6030Bank Charges

Ledgr's ledger endpoint takes account ids, not codes, so resolve them once with GET /api/accounts (with the X-Company-Id header) and cache the code→id map. Codes are stable; the ids are per company.

The daily journal

One balanced journal per trading day per location. A Tuesday taking R18 400 including VAT, of which R12 000 on card and R6 400 cash, with R900 of tips and R240 of card fees:

POST /api/ledger/journals
{
  "clientId": "3f0c9a52-6d1e-5b8a-9c47-2e1f0a7d4b19",
  "journalDate": "2026-08-04",
  "memo": "Aura POS takings — Main floor — Tue 4 Aug",
  "reference": "AURA-MAIN-20260804",
  "post": true,
  "lines": [
    { "accountId": "<1360 Petty Cash>",
      "debit": 6400.00, "description": "Cash takings" },
    { "accountId": "<1330 Yoco settlement>",
      "debit": 11760.00, "description": "Card takings net of fees" },
    { "accountId": "<6030 Bank Charges>",
      "debit": 240.00, "description": "Yoco processing fees" },
    { "accountId": "<4010 Sales — Goods>",
      "credit": 15217.39, "description": "Food & beverage revenue" },
    { "accountId": "<2130 VAT Output>",
      "credit": 2282.61, "description": "Output VAT" },
    { "accountId": "<2120 Tips Payable>",
      "credit": 900.00, "description": "Tips owed to staff" }
  ]
}

Note what the numbers do. Revenue is the VAT-exclusive figure — R17 500 of sales excluding tips, less the VAT it contains at 15%, is R15 217.39, with R2 282.61 of output VAT. Tips are a liability, not revenue: the restaurant is holding staff money, and booking it as income overstates both turnover and the VAT due on it. And the card line is net of fees, because that is what Yoco actually settles — with the fee shown as its own expense so the margin is honest.

reference is the label a person searches for — one value per location per day. It is not what stops a double posting; clientId is, below.

Not posting twice

Ledgr deduplicates a journal on its clientId: a UUID Aura generates once per location per day and sends with every attempt at that day. The first post creates the journal and answers 201; a retry with the same clientId — after a timeout Ledgr had in fact committed, or a job that ran twice — answers 200 with the journal that already exists, and posts nothing again. The simplest way to generate it is a name-based (version 5) UUID of the day's reference, so the same day always yields the same id without Aura having to store it.

A fresh clientId is a fresh journal, and a journal sent without one is never deduplicated — two identical journals on the same day are sometimes genuinely two journals. Aura keeps its own record of which sales it has already journalled, which is how it stays safe when offline sales arrive late; record beside it the clientId and the journal id Ledgr returned for each day.

The rules are the same for every create in the API; see retrying safely.

Stock, and why to leave it alone at first

Aura decrements its own stock on checkout and knows its own recipes. Ledgr has inventory too, with its own costing engine. Running both against the same physical stock means reconciling two sets of numbers forever.

Pick one. For a restaurant, Aura is usually the right owner — it knows the recipes and it is where the stock actually moves — and Ledgr receives the cost of sales as part of the daily journal, which is all the income statement needs. Keep Ledgr's inventory module for anything the till does not sell: cleaning supplies, crockery, kitchen equipment.

VAT and the VAT201

Once the daily journals are landing, Ledgr's VAT analysis and VAT201 pick the output VAT up automatically — it is a normal credit to 2130 like any other sale. Aura also has its own VAT201 export; use one or the other, not both, and if the books are in Ledgr then Ledgr is the one that will agree with the trial balance.

Before you go live

  • Push one quiet day and compare Ledgr's income statement against Aura's dashboard for the same date. They should agree to the cent on revenue and on VAT.
  • Check the balance sheet: tips should be a liability, and the Yoco settlement figure should match what actually lands in the bank two days later.
  • Reconcile the Yoco payout against 1330 once it clears. If the settlement account does not clear to nil over a week, the fee split is wrong.
  • Confirm no journal line landed on a header account — a header with a balance is the symptom of the mapping mistake above.
  • Revoke and re-issue the API key once, to prove that path works before you need it.

Where this stands

Honestly: Aura computes the journals today, in its own ledger, and Ledgr accepts them today over the public API. What is described here is the wiring between the two, and it is a job of a day or so on the Aura side — a nightly task that reads its own journal entries, maps the account codes, and posts one document per location per day. Nothing in Ledgr needs to change for it.

Common problems

What you seeWhat to do
401 on every call The key is wrong, revoked, or being sent the wrong way. It goes in the Authorization header, exchanged at /auth/token first. Generate a fresh key under Roles & Users → API Keys and paste it whole — a key is shown once and cannot be read back.
400 “X-Company-Id header is required” Send the company's id in an X-Company-Id header on every ledger call — see Authenticating.
403 with a scope named The key does not hold the scope the endpoint needs. Tick it on the key rather than widening the endpoint, and note that a key with no scopes can reach nothing.
403 with a module named Your plan does not include that module. A key cannot exceed the plan.
The journal posted twice for one day's trade Send the same clientId for a given day. Ledgr keys on it, so a retry after a dropped response returns the journal it already has rather than making a second one. A fresh clientId is a fresh journal, and the reference does not deduplicate anything.
The day will not balance Takings, VAT, tips and card fees have to reconcile to the amount banked. The refusal names the difference. A journal that does not balance is refused rather than posted, which is what keeps the trial balance meaningful.
An account id is refused It has to be an account in your chart of accounts. Read the chart back from the API and map against the ids you get, rather than hard-coding them — a chart is per company.
Tips are landing in revenue A tip is money you hold for staff, not turnover. Map it to a liability account. It matters on the VAT201 as well as the income statement.
Card fees are missing The gateway's fee is an expense and the amount banked is net of it. Post the gross takings and the fee separately, or the bank will never reconcile.
The bank feed and the till both created a transaction Expected, and they are meant to be matched rather than duplicated. Reconcile the banking side against the journal; see Banking.
429, with a Retry-After You are over the key's rate limit. Wait the number of seconds it gives you rather than retrying immediately.