Skip to main content

Xero Integration

This page explains how the Billing domain will send numbers to Xero.

Read it if you are:

  • an engineer who will build the finance export
  • the platform owner who needs to talk to finance about it
  • anyone who must explain why a number in Xero differs from a number in Jod

After reading you should be able to:

  • read a journal entry and say if it is correct
  • explain what finance does today by hand, and why
  • explain which Jod event creates which Xero line
  • explain what we must agree with finance before the first export

Related pages:

1. Words used on this page​

Words from our own system:

WordMeaning
EntitlementWhat a customer can still use on the platform. Measured in units.
InstrumentThe kind of entitlement. Today: placement and gig. Stored in billing_entitlements.instrument.
Placement creditOne unit that buys visibility: an ad, a careers job post, or a boost.
Gig creditOne cent of wage value the customer paid us in advance. 100 gig credits = SGD 1.00 of wage.
Ledger entryOne row in billing_ledger_entries. It records one change in units or money. Rows are only added, never edited.
LotOne purchase batch. One row in billing_entitlement_lots. It carries units, the money attached to those units, and an optional expiry date.
GrantA ledger entry that adds units. It creates one lot. Usually after a customer pays. Grants for trial credits, goodwill credits, and opening lots add units with no payment.
ReserveA ledger entry that moves units from available to reserved, so the customer cannot spend them twice.
ReleaseA ledger entry that moves reserved units back to available.
ConsumeA ledger entry that removes units because we delivered the service.
ExpireA ledger entry that removes leftover units when a lot passes its expiry date.
RefundA ledger entry that removes available units when we pay a customer back. Gig only, and always a full exit — every available credit, every lot.
AdjustA correction entry, or the zero-movement entry an expiry extension writes so the date change appears in the audit trail.
Platform feeOur margin on a gig purchase. The customer pays it on top of the wage value.

Words from accounting:

WordMeaning
RevenueMoney we have earned because we delivered the service.
Deferred revenueMoney we already collected, for a service we have not delivered yet. It is a debt until we deliver. Also called deferred income.
Principal liabilityStored value we owe back to the customer. Gig credits are this. It is not revenue, even after the customer pays.
BreakageRevenue we earn when a customer's credits expire unused. We keep the money and we no longer owe the service.
Output taxGST we charge the customer on a sale. We collect it and pay it to IRAS. It is never our revenue.
RecogniseTo move money from deferred revenue into revenue at the moment we earn it.

Xero's own words (organisation, chart of accounts, account code, manual journal, contact, tax rate, lock date, tracking category) are defined in section 9.

note

This page follows decisions D2 to D6 on the billing decisions page. The schema in jodapp-api/docs/db/billing.dbml implements them — see jodapp-api PR #1938. Older pages in this folder still describe placement credits as one shared pool with no expiry date; those parts are out of date until the spec rewrite lands.

2. What Xero is, and why Jod uses it​

Xero is cloud accounting software. Our finance team already uses it. It holds the official books of the company:

  • the bank accounts and what was paid in and out
  • what we owe and what we are owed
  • the profit and loss statement and the balance sheet
  • the GST return we file with IRAS

Xero is the record that auditors, the bank, and the tax authority look at. Jod is not an accounting system and should not try to be one.

The split of work is simple:

QuestionAnswered by
How many credits does this customer have left?Jod
Which shift used which credits?Jod
How much revenue did the company earn in September?Xero
What is our GST bill this quarter?Xero

One rule matters more than the rest. Xero never sees units. Xero only sees money. A customer having 16 placement credits means nothing to Xero. The SGD 128 we still owe them as a service does mean something to Xero. Our export turns unit movements into money movements.

3. Double-entry accounting in plain words​

You only need four ideas to read the rest of this page.

An account​

An account is a named bucket of money. A company keeps a list of them. Xero calls that list the chart of accounts. Every account has a short code, for example 200 or 820.

Accounts come in kinds. We use three kinds:

  • Asset — something we own. Example: the bank account.
  • Liability — something we owe. Example: gig credits we must give back.
  • Revenue — money we earned.

Debit and credit​

Debit and credit are just the left side and the right side of an entry. They are not "plus" and "minus". What they do depends on the kind of account:

Account kindA debit doesA credit does
Assetincreases itdecreases it
Liabilitydecreases itincreases it
Revenuedecreases itincreases it

Read the table with our own accounts:

  • Money arrives in the bank. That is a debit to the bank account.
  • We now owe the customer gig credits. That is a credit to the liability account.
  • A shift finishes and we earned our fee. That is a credit to a revenue account.

A journal entry​

A journal entry is a set of lines posted together on one date. Each line names one account and one amount. The total of the debits must equal the total of the credits. If they do not match, the entry is wrong and Xero will not accept it.

A short money example​

A customer buys 100 gig credits with a 20% platform fee. Following the invoice line tax policy, the stored value carries no GST and the fee carries 9% GST:

  • stored value: SGD 100.00
  • platform fee: SGD 20.00
  • GST on the fee: 20.00 × 0.09 = SGD 1.80
  • total the customer pays: SGD 121.80

The entry when the money arrives:

AccountKindDebitCredit
BankAsset121.80
Jod creditsLiability100.00
Deferred income — gig platform feeLiability20.00
GST payableLiability1.80
Total121.80121.80

Notice that no revenue appears yet. We have taken money but delivered nothing. Everything sits as a debt. Revenue only appears later, when a shift finishes or when credits expire.

note

The billing requirements notes show this same example with a total of SGD 121.90. That is an addition error in the notes. 120.00 + 1.80 = 121.80.

4. How Jod finance works in Xero today​

Everything in this section comes from the finance notes in billing requirements. It describes the gig business, which is the part running in production today.

The accounts finance uses​

Account name in XeroKindWhat it holds
Jod creditsLiabilityAll unused gig credits, for every customer, in one number
Deferred incomeLiabilityAll platform fees collected but not yet earned

There is no customer detail in either account. Xero holds one total.

The daily loop, Monday to Saturday​

  1. Ops exports the previous day's completed jobs as a payment summary file.
  2. Ops sends that file to finance.
  3. Finance pays the workers from the file.
  4. Finance posts one lump-sum entry to Xero for the day. The entry carries four totals:
    • total gig credits used
    • total insurance deducted from workers
    • total insurance we sponsored
    • total paid break we sponsored
  5. The entry names no customer. It only moves the single "Jod credits" total.

The monthly margin step​

At month end finance builds a list of all paid jobs for the month. They then type the service margin percentage against those jobs, taken from the customer's latest invoice. The revenue for the month is the total of that column. This is the moment the platform fee becomes revenue in Xero.

The monthly reconciliation​

Finance compares two numbers:

  • the closing balance of the "Jod credits" account in Xero
  • the total of all customer statements of account in the gig system

The difference is between SGD 1,000 and SGD 5,000 every month.

Why this process exists, and where it is weak​

Finance did not choose this process because it is ideal. They chose it because the system gives them nothing better. Their numbers must tie to real payments, and the payment file is the only reliable list they have.

Step todayWhy it existsWhere it is weak
Ops emails a daily payment fileThe platform has no finance exportThe file describes payouts, not credit movements. Credits actually deducted in the system can differ.
One lump sum to "Jod credits"One line per job would create thousands of lines a monthXero holds no customer detail, so no customer question can be answered from Xero
Margin typed in at month end from the latest invoiceDifferent purchases carry different fee ratesOne rate gets applied to a whole month. The rate that belonged to each purchase batch is lost.
Monthly compare of Xero against customer statementsNothing else proves the balance is rightA SGD 1k–5k difference every month, and nobody can point at the rows that cause it
CAFS and other credit adjustments handled by handAdjustments have no system record finance can pullEach adjustment is a chance to get the two systems further apart

The billing ledger fixes the root cause. It records every unit and every cent at the moment it moves. The export then reads the ledger, not a payout file.

5. The two export modes​

The billing design planned for two ways to get numbers into Xero. We build them in order.

Journal mode — first​

Our system reads the ledger, adds up one day of activity, and produces journal lines. Finance imports those lines into Xero as one manual journal per day. A manual journal is an entry a human or an integration posts directly, without an invoice behind it.

  • First version: we produce a CSV file. Finance uploads it in Xero.
  • Later version: we post the same lines through the Xero API.

Journal mode gives Xero correct totals. It gives Xero no customer names.

Invoice mode — future​

Our system creates one sales invoice per customer inside Xero, using billing_invoices and billing_invoice_lines as the source. Each invoice is linked to a Xero contact, which is Xero's word for a customer record.

Invoice mode gives Xero customer detail. It also gives Xero the accounts receivable and the GST return handling for free. It costs much more to build and to keep correct.

Which mode to choose​

QuestionJournal modeInvoice mode
What does finance get?Correct daily totalsCorrect totals plus a customer name on every sale
Who chases unpaid customers?Our systemXero
Does Xero know each customer's balance?NoYes
Does it touch the bank account or accounts receivable?NoYes
What must exist first?The ledger and an account mapA Xero contact for every company, plus invoice numbering owned by one side only
How hard is it to correct a mistake?Post a small reversing journalCredit note, then a new invoice
Build costLowHigh

Choose journal mode while finance is happy to answer customer questions from Jod. Move to invoice mode when finance wants Xero to hold customer balances, or when an auditor asks for invoice-level records inside Xero.

Both modes read the same ledger. The ledger stays the record of what we delivered and what we earned. Xero holds the money view of the same events.

6. Building blocks of the journal export​

6.1 The accounts we need in Xero​

Today finance uses two accounts. The export needs a few more. Splitting them costs nothing and makes each monthly check simple.

Example account nameKindWhat it holdsExample code
Customer receipts clearingLiabilityMoney received that the export has not turned into credits yet810
Jod creditsLiabilityGig stored value we owe back to customers820
Talent payouts payableLiabilityNet wages we owe workers but have not paid yet825
Insurance payableLiabilityInsurance premiums we owe the insurer826
Customer refunds payableLiabilityRefunded gig principal we owe back but have not paid out yet827
Deferred income — placement creditsLiabilityPlacement money collected, not yet earned830
Deferred income — gig platform feeLiabilityGig fees collected, not yet earned831
GST payableLiabilityOutput tax charged on sales840
Revenue — placement creditsRevenueEarned when placement credits are consumed200
Revenue — gig platform feeRevenueEarned when a shift finishes201
Revenue — credit breakageRevenueEarned when credits expire unused202
Credit adjustments / marketing costExpenseOnly used if finance chooses to book free credits as a cost when they are used — open question in the auditor section470

The codes above are examples. Finance owns the real codes. See section 6.6.

Two limits shape this list:

  • Xero blocks manual journals from posting to some system accounts, including accounts receivable, accounts payable, and bank accounts. The usual answer is a normal liability or asset account that you control, which is why "Talent payouts payable", "Customer refunds payable" and "Customer receipts clearing" exist above.
  • Only invoice mode can post to accounts receivable, because invoices create those lines themselves.

6.2 The mapping table​

This is the heart of the export. Each ledger entry type, for each instrument, maps to a fixed pair of accounts.

Ledger entryInstrumentDebitCreditAmount comes from
grantplacementCustomer receipts clearingDeferred income — placement, and GST payableInvoice line amounts on the posting
grantgigCustomer receipts clearingJod credits (stored value), Deferred income — gig platform fee, and GST payableInvoice line amounts on the posting
grantany, trial or goodwillnonenonethe lot carries SGD 0
grantany, opening balancenonenonethe money already sits inside the Xero balances — posting it again would double it
reserveanynonenoneno money moved
releaseanynonenoneno money moved
consumeplacementDeferred income — placementRevenue — placement creditsrecognised_revenue_cents
consumegig (stored value part)Jod creditsTalent payouts payable, and Insurance payableavailable_delta, split by the gig settlement
consumegig (fee part)Deferred income — gig platform feeRevenue — gig platform feerecognised_revenue_cents
expireplacementDeferred income — placementRevenue — credit breakagerecognised_revenue_cents on the expire entry
expiregignone todaynone todaygig lots carry no expiry date
refundgig (principal part)Jod creditsCustomer refunds payableavailable_delta — the credits paid back
refundgig (fee part)Deferred income — gig platform feeRevenue — gig platform feerecognised_revenue_cents on the refund entry
adjustexpiry extensionnonenonezero-movement entry — only the date changed
adjustany, reversing revenueRevenue accountDeferred income accountthe negative money delta on the entry. No such entry is designed yet — this row only says how one would post if a future correction design adds them.

Notes on the table.

Free credits produce no money lines. A trial or goodwill lot has units and SGD 0 attached. Consuming it recognises 0. Expiring it recognises 0. The export skips zero-value lines, so Xero sees nothing. That is correct: nobody paid us, so no money moved. (If finance chooses to book free credits as a marketing cost instead — the open question in the auditor section — this row changes and a reference price is needed.)

Opening lots never produce journal lines. At the gig flip, each company's opening lot carries real money — but that money is already inside Xero's "Jod credits" and "Deferred income" balances from years of manual entries. The grant that creates the lot only copies the position into our ledger. Posting it would count the same balances twice.

A refund posts no GST and touches no bank account. The fee is kept, so nothing tax-related reverses. The principal goes to "Customer refunds payable" because manual journals cannot post to bank accounts; when finance makes the actual bank transfer, they clear it against that account.

Gig stored value never becomes our revenue. It is the customer's money. When a shift finishes, that money leaves the "Jod credits" liability and becomes a debt to the worker and to the insurer. Only the platform fee crosses into revenue.

The gig payout split does not come from the billing ledger. The ledger knows how many credits were consumed. The split between net wage, insurance deducted, insurance sponsored, and sponsored paid break comes from the gig settlement records. The export joins both sources for the day. This mirrors what ops sends finance today.

6.3 Why reserve and release map to nothing​

A reservation moves units from available to reserved. Nothing else happens:

  • no money enters or leaves the company
  • the customer is not richer or poorer
  • we have not delivered anything

Accounting only records money movements. So a reserve produces zero journal lines, and so does a release. This is not missing data. Posting a reservation to Xero would state that something happened when nothing did, and the balance sheet would then be wrong until the shift finished.

Holds still matter inside Jod. They stop a customer from spending the same credits twice. That is a platform rule, not an accounting event.

6.4 Daily aggregation from the ledger​

One export run covers one calendar day for one legal entity. The steps:

  1. Pick the day. Day boundaries use the company's timezone, Asia/Singapore today. See the datetime conventions.
  2. Select ledger rows where occurred_at falls inside that day.
  3. Group them by entry_type and by billing_entitlement_id (the instrument).
  4. Sum the money columns for each group: deferred_revenue_delta_cents, recognised_revenue_cents, and available_delta for gig stored value. There are no separate fee columns — for a gig row, the deferred and recognised amounts are the fee, because the principal is a liability, not revenue.
  5. Look each group up in the mapping table from section 6.2.
  6. Write one journal line per account, per group.
  7. Check that debits equal credits before writing anything out.

One day becomes about ten lines, not thousands. Finance keeps the lump-sum shape they already work with. The difference is that the numbers now come from the ledger instead of a payout file.

6.5 Why we never recompute history​

Every ledger row stores the money amounts that were true at that moment. recognised_revenue_cents on a consume row is the amount we recognised then. It is copied at that moment and never changes afterwards.

The export only reads those stored amounts. It never recalculates them from today's balances. This matters because balances keep moving:

  • the lot the credits came from may be empty now
  • the customer may have bought more credits at a different price
  • an adjustment may have changed the account since

If the export recalculated, then re-running last March would produce different numbers than March produced. The books would drift away from the ledger and nobody could prove which version is right. Reading stored amounts means March always exports the same numbers, forever.

6.6 Account codes are configuration, not code​

Account codes must never be written into Ruby code. Reasons:

  • Each legal entity has its own Xero organisation, with its own chart of accounts.
  • Finance can rename or renumber accounts without telling engineering.
  • A new country will need a different set of accounts and a different tax treatment.

Keep the map in a configuration table, keyed by legal entity, entry type, instrument, and the role of the line (debit or credit). Adding a country then means adding rows, not shipping code. Every export run copies the codes it used into the run record, so an old run can still be explained after the map changes.

Each Xero organisation is one tenant in the API. Requests carry the organisation in the xero-tenant-id header. Store that id next to the legal entity, alongside the account map.

6.7 GST stays at invoice time​

GST is charged when we sell, not when we deliver. Those are different days.

EventDayGST
Customer buys 100 gig credits with a 20% fee1 September9% on the SGD 20 fee. Stored value is not taxed.
A shift consumes SGD 80 of those credits15 Septembernone
Leftover placement credits expire30 Novembernone
The company exits gig and we refund the restany daynone. The fee is kept, and the principal never carried GST.

So the recognition journal — consume, expire, adjust — carries no tax at all. Every line goes out with no tax. In the CSV the tax rate column says the org's no-tax rate. In the API the manual journal sets LineAmountTypes to NoTax.

Only the sale carries output tax, and the sale is the grant. Whether the export posts the sale side at all is a choice for finance. Two options:

OptionWho posts the sale and the GSTWhat the export posts
AFinance, as they do todayRecognition only: consume, expire, adjust
BThe export, through the clearing accountEverything, including grants

Option B has one useful property. If both sides are posted, the "Customer receipts clearing" account must return to zero. A balance left in it means a payment we never granted credits for, or a grant with no payment behind it. That is a real control, and it is free.

If finance picks option B, ask their accountant one question first: posting the GST amount straight to the GST account may not feed the GST return correctly, because Xero usually builds that return from tax rates on the lines. Get this confirmed with finance before the first run.

6.8 Safe to run twice, and closed periods​

Nobody should be able to double-post a day. Two mechanisms cover this.

Inside Jod. Every export run gets a record and a unique key, built from the legal entity, the date, and the run type. Re-running a day that already posted returns the existing run instead of creating a new one. The run record stores the file we produced and, once posted, the manual journal id Xero gave back.

At the Xero API. Xero accepts an Idempotency-Key header, up to 128 characters. It lets you retry a request safely, without the risk of the same journal being created twice. Send the export run key as that header. A network timeout then costs nothing: retry with the same key.

Closed periods. Finance closes a month when it is reported. In Xero this is a lock date, set in the financial settings. There are two of them: one for all users, and one for all users except advisers. Once a lock date is set, nobody can add or change a transaction dated on or before it.

Our rules around that:

CaseRule
Export for a day inside an open periodPost normally, dated that day
Export for a day on or before the lock dateRefuse. Do not post. Raise it for a human.
A late ledger entry for a closed monthPost it in the current open period. Name the original date in the narration.
Finance asks to change a posted dayNever edit. Post a reversing journal in an open period.

The ledger already works this way. You cannot edit a row, only add a correcting one. Xero lock dates enforce the same habit on the other side.

6.9 The verification artifact​

Every export run produces a report a human can read. Finance approves the report before anything is posted, and checks the same report again after posting. This follows the repo rule that any export or migration must name the artifact a human approves.

The report contains:

PartWhy finance needs it
The day, the legal entity, and the run keyTo know exactly what this run covers
One row per journal line: account code, account name, debit, creditThis is what will land in Xero
Debit total, credit total, and the differenceThe difference must be 0.00
Ledger row count and the id range readTo prove no row was read twice or missed
The unit movements behind each money lineTo connect SGD 34.00 back to 4 placement credits
Rows skipped, with the reasonZero-value trial credits, reserves, releases
Closing balances after this run: Jod credits, both deferred income accountsTo compare against the same accounts in Xero
Which account map version was usedTo explain old runs after the map changes

Run the export in dry-run mode first. It writes the report and nothing else. Only after approval does the run post. After posting, compare the report's closing balances against Xero's account balances. They must match.

6.10 What happens to the monthly reconciliation​

Today finance compares the Xero "Jod credits" balance against the total of all customer statements, and finds a SGD 1k–5k difference every month.

After journal mode, both numbers come from the same ledger:

  • the customer statement is ledger rows for one account
  • the Xero balance is the total of the daily journals, which are also ledger rows

So they agree by construction. Any remaining difference has exactly one possible source: an entry a human posted straight into Xero, outside the export. That is a short list to check, and the daily reports say which day to open.

The monthly reconciliation does not disappear. It gets much smaller, and a difference now points at a specific day and a specific account instead of at a whole month.

7. Worked example: one day traced into Xero​

Date: 15 September 2026, Asia/Singapore. Four ledger entries happened.

The starting position​

LotCompanyInstrumentBoughtUnits thenMoney attachedExpires
L-11Acme Foodsplacement1 Sep 202610 creditsSGD 100.001 Sep 2027
L-12Bright Martplacement3 Sep 202620 creditsSGD 160.003 Sep 2027
L-07Sunrise Cafeplacement15 Sep 202510 creditsSGD 80.0015 Sep 2026
G-03Crispy Pratagig10 Sep 202610,000 credits (SGD 100.00 of wage)SGD 20.00 fee, 2000 bpsnone

Sunrise Cafe already used 4 of their 10 credits earlier in the year. They have 6 left.

The four ledger entries​

Entry 9001 — consume, placement, Acme Foods. They post one careers job. It costs 1 credit, taken from lot L-11.

  • money per unit in L-11: 100.00 / 10 = SGD 10.00
  • recognised_revenue_cents = 1 × 1000 = 1000 (SGD 10.00)

Entry 9002 — consume, placement, Bright Mart. They start a 3-day ad campaign. It costs 3 credits, taken from lot L-12.

  • money per unit in L-12: 160.00 / 20 = SGD 8.00
  • recognised_revenue_cents = 3 × 800 = 2400 (SGD 24.00)

Entry 9003 — expire, placement, Sunrise Cafe. Lot L-07 reaches its expiry date with 6 credits unused.

  • money per unit in L-07: 80.00 / 10 = SGD 8.00
  • available_delta = −6 units
  • recognised_revenue_cents = 6 × 800 = 4800 (SGD 48.00), recognised as breakage

Entry 9004 — consume, gig, Crispy Prata. A worker finishes an 8-hour shift at SGD 10.00 per hour. The worker is a Bronze member, so the insurance premium is deducted from their wage.

  • wage: 8 × 10.00 = SGD 80.00, so available_delta = −8000 gig credits
  • insurance: 8 × 0.35 = SGD 2.80
  • net payable to the worker: 80.00 − 2.80 = SGD 77.20
  • platform fee earned: 8000 × 2000 / 10,000 = 1600 cents, so recognised_revenue_cents = 1600 (SGD 16.00). For a gig entry this amount is the fee — the wage part is a liability movement, never revenue.

The journal finance receives​

#AccountCodeDebitCreditFrom
1Deferred income — placement credits83034.00entries 9001 + 9002
2Revenue — placement credits20034.00entries 9001 + 9002
3Deferred income — placement credits83048.00entry 9003
4Revenue — credit breakage20248.00entry 9003
5Jod credits82080.00entry 9004
6Talent payouts payable82577.20gig settlement for 9004
7Insurance payable8262.80gig settlement for 9004
8Deferred income — gig platform fee83116.00entry 9004
9Revenue — gig platform fee20116.00entry 9004
Total178.00178.00

Checks a human can do in ten seconds:

  • 10.00 + 24.00 = 34.00, so lines 1 and 2 agree with the two consumes
  • 77.20 + 2.80 = 80.00, so the gig credits used equal what leaves the company
  • 80.00 × 20% = 16.00, so the fee matches the lot's rate
  • debits 178.00 equal credits 178.00

Balances after the day:

  • lot G-03 has SGD 20.00 of wage value left, and SGD 4.00 of fee still deferred. 20.00 × 20% = 4.00, which is consistent.
  • lot L-07 is closed. Its units are gone and its money is now revenue.
  • Sunrise Cafe's SGD 48.00 became revenue on 15 September, not spread over the year before.

The same day as a CSV​

Xero's manual journal import uses one amount column. Debits are positive and credits are negative. The narration and date appear on the first row only. A row with a blank date and blank narration belongs to the journal above it.

Narration,Date,Description,AccountCode,TaxRate,Amount
Jod billing 2026-09-15 run 412,2026-09-15,Placement credits consumed (2 entries),830,No Tax,34.00
,,Placement revenue recognised,200,No Tax,-34.00
,,Placement credits expired (lot L-07),830,No Tax,48.00
,,Breakage revenue,202,No Tax,-48.00
,,Gig credits consumed (1 shift),820,No Tax,80.00
,,Net wage payable to talent,825,No Tax,-77.20
,,Insurance premium deducted,826,No Tax,-2.80
,,Gig platform fee earned,831,No Tax,16.00
,,Gig platform fee revenue,201,No Tax,-16.00

The amounts add up to zero, which is what a balanced journal looks like with this sign convention.

Two rules for the file:

  • Download the template from Xero and copy its header row exactly. Some Xero templates put a * in front of the required column names. Do not rename or reorder the columns, or the import fails.
  • The value in the tax rate column must match a tax rate name in that Xero organisation. Ask finance for the exact wording of their no-tax rate.

One template file holds up to 300 lines. Ten lines a day is far below that, so one file per day is safe.

The same day through the API​

Later we post the same lines to POST https://api.xero.com/api.xro/2.0/ManualJournals. Shortened to the first four lines:

{
"ManualJournals": [
{
"Narration": "Jod billing 2026-09-15 run 412",
"Date": "2026-09-15",
"Status": "POSTED",
"LineAmountTypes": "NoTax",
"JournalLines": [
{ "AccountCode": "830", "LineAmount": 34.00, "Description": "Placement credits consumed" },
{ "AccountCode": "200", "LineAmount": -34.00, "Description": "Placement revenue recognised" },
{ "AccountCode": "830", "LineAmount": 48.00, "Description": "Placement credits expired (lot L-07)" },
{ "AccountCode": "202", "LineAmount": -48.00, "Description": "Breakage revenue" }
]
}
]
}

Request headers that matter:

  • xero-tenant-id picks which Xero organisation receives the journal
  • Idempotency-Key: billing-export-412 makes a retry safe

Status can be DRAFT or POSTED. Sending DRAFT first is a useful extra safety step: finance opens the draft in Xero, checks it against our report, and posts it themselves.

8. What "the first Xero export" means as a milestone​

The milestone is small on purpose: one approved CSV, for one day, imported into Xero by finance, with the report matching Xero afterwards. No API, no invoices, no automation on a schedule.

Already decided​

QuestionDecision
Where does truth live?billing_ledger_entries, append only
Do placement credits use lots?Yes. Both instruments use one lot mechanism.
Which lot is spent first?Earliest expiry, then earliest purchase, then id. Gig lots have no expiry, so gig is plain first-in-first-out.
When do we recognise breakage?On the expiry date, in full. Not spread out by estimate. We have no usage history yet to base an estimate on.
What is gig stored value?A refundable debt to the customer. Never our revenue.
Do reserve and release reach Xero?No. No money moves.
When is GST charged?At sale time, on the invoice. Never in the recognition journal.
What do free trial credits recognise?SGD 0.00, because the attached money is SGD 0.00
Can a customer refund part of their credits?No. A gig refund is a full exit: every available credit, every lot. The fee is kept and recognised.
Do companies carry placement balances into launch?No. Placement launches at zero — there is no old placement system. Manual invoices stay in Xero only.
What does the gig flip post to Xero?Nothing. The opening balances are already inside the Xero accounts, so the export skips the grants that create opening lots.

What finance supplies before the first run​

None of these block engineering. The export code reads them from configuration, so the work can start now and the values can arrive later.

  • the real account codes for every row in section 6.1
  • the exact name of the no-tax rate in their Xero organisation
  • whether the export posts the sale side too, which is option A or option B in section 6.7
  • which existing manual entry the export replaces, so the same day is not recorded twice
  • who approves the report, and by when each day
  • the lock date policy: which day of the month a period closes

Questions for the auditor​

Finance passes these to the auditor. The answers must arrive before the first export is posted. None of them block the engineering work.

QuestionOur positionDecisionStatus
When do we recognise breakage?On the expiry date, in full. We have no usage history to estimate with, and SFRS(I) 15 allows this when breakage cannot be estimated.D2✅ Finance head agreed (Slack, 2026-08-06). Get the auditor's written confirmation before the first export.
Does a gig refund need a GST adjustment?No. GST was charged on the platform fee only, and the fee is kept on refund. The principal never carried GST. So a refund moves no tax.D4Open — not yet asked.
When free credits are used, do we book marketing expense and revenue at the normal price (gross-up), or book nothing?Either works on lot data. Free lots carry SGD 0 and are identifiable by their source (Billing::CreditAction), so the export can book them at a reference price if finance chooses the gross-up. If chosen, the grant must snapshot the product's list price as that reference.—Open — finance head proposed the gross-up (Slack, 2026-08-06). Needs her confirmation and the reference price choice.

Two answers arrived on 2026-08-06 without being asked (Slack, finance head):

  • Gig refunds: finance stated on their own that the whole remaining fee becomes revenue when credits are refunded — the same rule as D4.
  • Averaging: the auditor accepts pooled averages "as long as we can prove how the average per credit is computed". We keep lot-level records anyway. Any average finance wants is computed from the ledger and the allocation rows — which is exactly the proof the auditor asks for. Permission to average does not require averaging; see the derivability point in D2.

The audit schedules​

The auditor's written requirements arrived on 2026-08-07, via finance. Vincent (the audit partner — the person who signs the audit) had no objection to the recognition method. At every year-end audit we must hand over four tables, per customer. Accountants call each one a movement schedule: opening balance + what came in − what went out = closing balance. The auditor re-computes them, ties the totals to Xero, and traces samples back to source documents.

All four are reports over tables that already exist. None needs new bookkeeping.

#The auditor asks forWhat it isWhere it comes from
1Opening balance, new additions, consumption, adjustments, closing balance of JOD credits — in quantities and valueCredit movement table, in units and dollars. The two columns must agree: value ÷ units = price per credit.Ledger entries per account grouped by entry type. Grants are additions; consumes are consumption; expire, refund and adjust are adjustments.
2The same movement table for deferred incomeOpening + new sales − revenue transferred out = closing, in dollars. "Transferred out" = recognised.The two ledger money columns (deferred_revenue_delta_cents, recognised_revenue_cents) summed per period.
3Detailed calculation of average price for each customerRemaining value ÷ remaining credits, with the history behind the number.Derived from lots at any date. The ledger replay in id order is the proof of how the number was built.
4Detailed working of each customer's revenue for the whole yearEvery revenue event: date, credits moved, price applied, amount, and which purchase batch the credits came from.Recognition ledger entries plus their allocation rows.

Two rules that follow:

  • We recognise revenue at the actual price of each purchase batch (accountants call this specific identification), not at a blended average. Requirements 1, 2 and 4 are identical under any method. Requirement 3 becomes a derived figure we print on request. Tell the auditor the method before year end — auditors accept either method; what they object to is surprise, or changing method mid-year.
  • The year-one opening balances in table 1 are the migration's flip-day opening lots. The migration's verification report — the dry-run a human approves before any writes — is therefore audit evidence, not only an internal safety step.

Order of work​

  1. Daily aggregation from the ledger, plus the report. No file, no Xero.
  2. CSV output, checked by finance against their own manual numbers for the same days.
  3. Finance stops posting the daily entry by hand.
  4. API posting, with the export run key as the Idempotency-Key.
  5. Invoice mode, only when finance wants customer balances inside Xero.

Steps 1 and 2 can run beside the current manual process for a few weeks. Running both and comparing them is the cheapest way to find a mapping mistake.

9. Xero glossary​

Xero termMeaning
OrganisationOne set of books inside Xero, for one legal entity. Each has its own accounts and its own GST setup. The API calls it a tenant, and requests name it in the xero-tenant-id header.
Chart of accountsThe list of every account in an organisation.
Account codeThe short code of an account, for example 200. Codes are set by whoever set up the organisation, so they differ between organisations.
Manual journalA journal entry posted directly, without an invoice or a bill behind it. Its status can be DRAFT, POSTED, DELETED or VOIDED. Each line carries an account code, an amount, and an optional description.
Journal lineOne line of a manual journal. In the API the field is LineAmount, where debits are positive and credits are negative.
ContactA customer or supplier record. Invoice mode needs one contact per company. Journal mode needs none.
Tax rate / tax typeThe GST treatment of a line. The API uses a tax type code, not the display name. Singapore codes exist in the API, for example SROUTPUT for standard-rated supplies. Confirm the codes with finance, because each organisation configures its own.
Line amount typesWhether the amounts on a journal include tax, exclude tax, or carry no tax. We use NoTax for recognition journals.
Lock dateA date set in financial settings. Nobody can add or change transactions dated on or before it. There are two: one for all users, and one for all users except advisers. Only the adviser role can change them.
Tracking categoryAn optional label on a line, used to split reports, for example by region. A line can carry at most two. Each category holds a limited number of options, so it does not work as a way to tag every customer.
System accountAn account Xero creates and controls, such as accounts receivable, accounts payable, and bank accounts. Manual journals cannot post to several of these, which is why the export uses its own liability accounts instead.

10. Sources checked​

Xero facts on this page were checked against these sources on 6 August 2026:

Anything not on this list is our own design, not a Xero rule. Before the first run, check the account codes, the tax rate names, and the GST return treatment with finance.