Open-balance check
The open-balance check tells you whether a guest owes an operator money, and how much, in each of the operator’s workspaces (buildings). Use it before you offer something that depends on the guest being up to date, such as rebooker terms for returning residents.
Lavanda only reports the balance. Deciding who is eligible for what stays with you.
Note: The check is being rolled out one client at a time. Ask your Lavanda representative to enable it for your API client before you build against it.
Key concepts
Section titled “Key concepts”- Contact: a guest or resident in an operator’s CRM. Each contact has a Lavanda
uniqueReference, and may also carry your own CRM id inexternalCrmId. - CRM namespace: the set of contacts that belongs to one operator. Access to the check is granted per CRM namespace. An operator can have more than one.
- Balance: what the contact owes in one workspace and one currency: issued invoices minus receipts and the credit notes raised against those invoices. A contact has one balance per workspace and currency they have been invoiced in.
- Status:
OWINGorNOT_OWING, worked out by Lavanda for each balance and for the contact overall.
Look up a contact’s balances
Section titled “Look up a contact’s balances”The check is a read-only field, arBalances, on the existing CrmContact type. There is no separate
query: you find the contact with a query that already exists, and select arBalances on it.
By Lavanda contact reference
Section titled “By Lavanda contact reference”Use crm.contact when you hold the contact’s Lavanda uniqueReference.
query ContactArBalances($id: ID!) { crm { contact(id: $id) { id arBalances { status balances { workspace { id name } balance { amountMinor currency } status updatedAt } } } }}{ "id": "<contact-unique-reference>"}By your own CRM id
Section titled “By your own CRM id”If you store your own CRM id on the contact, search for it with crm.contacts and select the same
field on each match.
query ContactArBalancesByCrmId($filters: ContactsFiltersInput) { crm { contacts(filters: $filters) { nodes { id arBalances { status balances { workspace { name } balance { amountMinor currency } status updatedAt } } } } }}{ "filters": { "externalCrmId": { "eq": "<your-crm-id>" } }}An empty nodes list means no contact you can see has that CRM id. Treat it as “not found”.
Check several contacts in one request
Section titled “Check several contacts in one request”Use GraphQL aliases to check several contacts in one HTTP request. Each alias is answered on its own,
so an error on one alias does not stop the others. Within one alias, each contact is answered on its
own too: if a crm.contacts filter matches several contacts and the check fails for one of them,
only that contact’s arBalances is null, with an error whose path points to it. The other contacts
in nodes keep their balances.
A request can carry at most 15 aliases, counted anywhere in the query, nested ones included. The router rejects more, so split larger batches across requests.
This is also how you check a guest who appears in more than one of an operator’s CRM namespaces.
Lavanda does not combine balances across namespaces for you. If you need “owes money in any
namespace”, ask for each contact and treat the guest as owing when any of the results is OWING.
query GuestArBalancesAcrossNamespaces( $firstNamespace: ContactsFiltersInput $secondNamespace: ContactsFiltersInput) { crm { firstNamespace: contacts(filters: $firstNamespace) { nodes { id arBalances { status balances { workspace { name } balance { amountMinor currency } status updatedAt } } } } secondNamespace: contacts(filters: $secondNamespace) { nodes { id arBalances { status balances { workspace { name } balance { amountMinor currency } status updatedAt } } } } }}{ "firstNamespace": { "externalCrmId": { "eq": "<your-crm-id>" }, "crmNamespace": { "id": { "eq": "<first-crm-namespace-id>" } } }, "secondNamespace": { "externalCrmId": { "eq": "<your-crm-id>" }, "crmNamespace": { "id": { "eq": "<second-crm-namespace-id>" } } }}The same works with crm.contact: alias it once per uniqueReference you want to check.
Reading the response
Section titled “Reading the response”| Field | Type | Meaning |
|---|---|---|
arBalances.status |
AccountingArBalanceStatus! |
OWING when any balance is above zero. NOT_OWING otherwise, including when there are no balances. |
arBalances.balances |
[AccountingArBalance!]! |
One entry per workspace and currency. Empty when the contact has no issued documents. |
balances.workspace |
Workspace |
The workspace the balance belongs to. Can be null; see Workspace details. |
balances.balance |
AccountingMoney! |
amountMinor (in minor units, so 12500 is 125.00) and currency. Signed: negative means a credit. Never floored at zero. |
balances.status |
AccountingArBalanceStatus! |
OWING when this balance is above zero. |
balances.updatedAt |
DateTime! |
When this balance last changed. |
An example response, with made-up values:
{ "data": { "crm": { "contact": { "id": "<contact-id>", "arBalances": { "status": "OWING", "balances": [ { "workspace": { "id": "<workspace-a-id>", "name": "Example House North" }, "balance": { "amountMinor": 12500, "currency": "GBP" }, "status": "OWING", "updatedAt": "2026-10-20T09:15:00Z" }, { "workspace": { "id": "<workspace-b-id>", "name": "Example House South" }, "balance": { "amountMinor": -3000, "currency": "GBP" }, "status": "NOT_OWING", "updatedAt": "2026-10-02T14:40:00Z" } ] } } } }}How to read it:
- A credit never offsets debt elsewhere. The guest above is
OWINGbecause they owe 125.00 in one workspace, even though they are 30.00 in credit in another. Balances are not netted across workspaces or currencies. - Balances are signed and never floored. A negative
amountMinoris money the operator holds for the guest. Lavanda does not round it up to zero. - No balances means
NOT_OWING. A contact who has never been invoiced has an emptybalanceslist and the statusNOT_OWING. updatedAtis when the balance last changed. It does not tell you whether a very recent invoice has been counted yet. See Known gaps.
Errors
Section titled “Errors”Errors come back in errors[], with the code in extensions.code. When the check fails for a
contact, you get no balances for it: arBalances is null, with an error whose path ends in
arBalances. The rest of the contact (id, name and so on) still resolves, and so do the other
contacts in the same list. A null arBalances means unknown, never not owing.
If Lavanda cannot answer at all (for example a timeout), every contact in the response can have
arBalances: null with a single error that does not point at any one contact. Treat a null arBalances
as unknown whether or not an error points at it.
If a contact comes back with arBalances: null and a FIND_CONTACT_ERROR error for it, your client cannot
read that contact. Treat it as not found.
| Code | Meaning | What to do |
|---|---|---|
AR_BALANCE_UNAVAILABLE |
Lavanda cannot answer for this contact right now. The check may be temporarily unavailable, or the contact is not ready to be checked yet. | Treat the guest as unknown. Never treat this as NOT_OWING. Retry later or fall back to a manual check. |
UNAUTHORIZED_ERROR |
Your client is not allowed to see this contact’s balance. | Ask Lavanda for the grant on that CRM namespace. Treat the guest as unknown. |
AUTHORIZATION_UNAVAILABLE_ERROR |
Lavanda could not check your access. | Retry. Treat the guest as unknown until you get an answer. |
FIND_CONTACT_ERROR |
No contact you can read matches. From crm.contact, or from crm.contacts when a match exists but your client cannot read its profile. |
Treat as not found. If you expected a match from crm.contacts, check your client has the view_profile grant. |
Not found looks the same whether the contact is unknown or belongs to someone else. A contact in
another operator’s CRM namespace returns exactly the same response as a contact that does not exist:
FIND_CONTACT_ERROR from crm.contact, or an empty nodes list from crm.contacts. This stops
callers from discovering other operators’ guests.
An error on a balance’s workspace is different: see the next section. It never makes the balance
unknown.
The safe rule: only NOT_OWING means not owing. An error, a null, or a missing contact never does.
Workspace details on each balance
Section titled “Workspace details on each balance”Each balance carries the Workspace it belongs to, so you can select the workspace’s current name
and other workspace fields.
Reading a workspace’s details needs the awc_workspaces_read grant on that workspace. Without it, that
balance comes back with workspace: null and a USER_NOT_ALLOWED_ERROR error whose path points to it
(for example ["crm", "contact", "arBalances", "balances", 1, "workspace"]). The balance, status
and updatedAt of that entry are still returned, and the contact’s overall status still covers it.
So an error on workspace does not mean the balance is unknown. Read the amount and status as normal,
and ask Lavanda for awc_workspaces_read on the workspaces whose names you need.
What the balance includes
Section titled “What the balance includes”- Issued documents only. Drafts do not count. The balance is issued invoices minus receipts and the credit notes raised against those invoices.
- Payments count once their receipt is issued. A payment moves the balance when Lavanda issues its receipt, not when the money is taken.
Known gaps
Section titled “Known gaps”- Lag on busy billing days. A new invoice or receipt can take a while to reach the balance, and
during a large billing run it can be delayed by hours. Until it arrives, the balance and its
updatedAtlook exactly as they did before, soupdatedAtcannot tell you that something is still on its way. If a decision depends on very recent invoices, check again later. - Group-invoiced residents. When a resident is billed through a company, the debt sits on the company, not on the resident. The resident’s check does not show it.
- Not yet in the balance: write-offs, standalone credit notes and refund reversals. Each will be added to the balance when it ships, and we will update this page.
Access you need
Section titled “Access you need”Lavanda grants access to your API client per CRM namespace. For each operator namespace you check, your client needs:
| Grant | Needed for |
|---|---|
view_ar_balance |
Reading arBalances on the namespace’s contacts |
view_profile |
Reading the contact. Needed for both crm.contact and crm.contacts |
search_contacts |
Looking contacts up with crm.contacts (by your own CRM id) |
awc_workspaces_read |
Reading workspace details on each balance. Granted per workspace, not per namespace. |
You only need search_contacts if you look contacts up by your own CRM id. Ask your Lavanda
representative to set these up.
Change to crm.contact errors for denied lookups
Section titled “Change to crm.contact errors for denied lookups”crm.contact used to tell you when a contact existed but your client could not see it. It now returns
the not-found error in that case. The same applies wherever a contact is resolved for another query,
for example when crm.contacts fetches a contact’s profile fields.
| Case | Before | Now |
|---|---|---|
| Contact does not exist | FIND_CONTACT_ERROR “Failed to find contact” |
No change |
| Contact exists but your client cannot see it | USER_NOT_ALLOWED_ERROR “Unable to show contact details” |
FIND_CONTACT_ERROR “Failed to find contact” |
If your integration treats USER_NOT_ALLOWED_ERROR from crm.contact as “this contact exists”,
update it: a missing grant now looks the same as an unknown contact. Queries for companies, leads and
contact relationships still return USER_NOT_ALLOWED_ERROR when access is denied.