Skip to content

Queries

Optional status filters by exact state (confirmed, pending, failed), and search matches the reference or description.

Results are scoped to your account’s default currency, so totalCount always reconciles with the matching dashboard KPI. Accounts are single-currency today, so this scope is a no-op until multi-currency is enabled.

query {
transactions(limit: 10, offset: 0, status: "confirmed", search: "acme") {
transactions {
id
reference
amount
tokenSymbol
status
transaction_hash
timestamp
}
totalCount
}
}

Both transactions and invoices below are scoped to your account’s current environment (test or live — see Test Mode), driven server-side by the merchant’s mode, not a client parameter. A transaction with no wallet yet (its environment isn’t knowable) is returned in both environments rather than dropped from either.

A lightweight query for the “N hidden — switch modes” hint shown above list pages. Returns counts for both environments regardless of which one is currently active, so the caller can compare against what’s on screen.

query {
environmentCounts {
testTransactions
liveTransactions
testInvoices
liveInvoices
}
}
query {
invoices(status: "sent", limit: 10) {
invoices {
id
invoice_number
status
amount
discountAmount
tax_amount
total
amount_remaining
customer_name
customer_email
due_date
invoiceCurrency
}
totalCount
}
}
  • total is what the invoice asks for: the subtotal less the invoice discount, plus tax and tip, before any payment. It is the figure the hosted invoice page, the PDF and the dashboard call Total, and the one the balance is measured against. Use it when you mean “the invoice’s amount”.
  • amount is the subtotal only: the line items before the invoice discount and tax. discountAmount is the invoice-level discount (a coupon, or an imported invoice’s discount), 0 when there is none.
  • Tax already included in a tax-inclusive import’s amounts (includedTaxAmount) is inside total, never added on top.
  • amount_remaining is what is still owed after payments and credit notes.
  • All money fields are decimal strings, such as "1100.00".
  • The invoice CSV export (exportInvoicesCSV) has the same split: amount is the subtotal, beside tax_amount, tip_amount and discount_amount, and total is the invoice total.

When a merchant takes card payments through their own Stripe account, an invoice exposes them. See Card Payments for the merchant’s side.

query {
invoices(status: "paid", limit: 10) {
invoices {
id
cardPaymentsAllowed
cardPaymentsEffective
providerPayments {
id
rail
status
amount
currency
platformFeeAmount
amountRefunded
livemode
succeededAt
needsRefundReason
disputeStatus
}
}
}
}
  • cardPaymentsAllowed is the invoice’s own setting: true, false, or null while it follows the merchant’s default. cardPaymentsEffective is that setting or the default, whichever applies. It is the merchant’s intent only: a card can be taken only with a working Stripe connection too.
  • providerPayments lists the card payments made or attempted against the invoice, newest first. status is requires_payment, processing, succeeded, failed or canceled, and only succeeded reduces the balance. Amounts are strings at the currency’s own precision.
  • needsRefundReason is void_invoice or overpaid when money arrived that the invoice did not need. Plirin does not refund it by itself.
  • disputeStatus is null when there is no dispute, otherwise warning (an inquiry), inquiry_closed, needs_response, under_review, won or lost, with disputedAmount, disputeFeeAmount and disputeDueBy beside it. See Card Disputes.
  • To read one invoice, use REST GET /api/v1/invoices/{id}, which carries card_payments_allowed and provider_payments (see the SDKs); the single-invoice GraphQL query is denied to API keys.
  • Test and live stay apart: an API key reads only its own environment’s payments, and livemode says which a payment is.

An invoice imported from another tool is an ordinary invoice with extra fields, and the list can be filtered by origin.

query {
invoices(imported: true, limit: 20) {
invoices {
id
invoice_number
status
importedAt
importId
importReminderNextAt
importReminderEveryDays
importRemindersSent
importChaseEndedReason
includedTaxAmount
}
totalCount
}
}
  • importedAt is when the invoice was imported (ISO-8601 UTC), and is null for every invoice made in Plirin. The invoice’s creation_date is its original issue date, and sent_at is the import time. importId is the import it came from.
  • importReminderNextAt is when the import chase sends its next reminder. null means no next reminder: the chase was stopped, never started or has ended. importReminderEveryDays is the interval in days, and is null for an invoice that follows your normal reminder schedule (one that was not overdue at import). importRemindersSent counts reminders sent so far; the chase ends after 6.
  • importChaseEndedReason is limit_reached once the chase has sent its last reminder, and null while it can still run. A paused chase (import switched off, or a plan without automatic reminders) keeps all its fields; nothing is sent until it can run again.
  • includedTaxAmount is the tax already inside an imported invoice’s stated amounts (“Includes tax”). It is display only: tax_amount is 0 for such an invoice and no total or balance reads it. null for every other invoice.
  • The invoices filters: imported: true returns only imported invoices, imported: false only those made in Plirin, chaseEnded: true only invoices whose chase has ended (false only those where it has not). Leave a filter out to ignore it.
  • The invoice CSV export (exportInvoicesCSV) has a trailing imported column, yes or no.
  • Imported invoices count toward what is owed (outstanding, overdue, aging) but not toward invoiced-in-period figures or the VAT report.
  • Only GraphQL carries these fields. The REST invoice body (/api/v1/invoices) and both SDKs have no imported marker, so an integrator on REST sees an old created_at and an overdue status and cannot tell an import from any other invoice; use this query. The public checkout payload (/pay/{slug}) has an imported boolean, which the hosted page uses to hide the promo code field. A reminder sent through REST POST /api/v1/invoices/{id}/remind counts toward an imported invoice’s chase of 6.
  • Import operations are dashboard-only and are denied to API keys: see Invoice import.

The import screen reads two more queries, also dashboard-only: invoiceImportTemplate (the CSV template text) and invoiceImportSettings { enabled allowanceRemaining autoChaseAvailable pdfImportAvailable lastChase { mode everyDays } }. allowanceRemaining is null when the plan has no cap.

query {
creditNotesForInvoice(invoiceId: "550e8400-e29b-41d4-a716-446655440000") {
id
creditNoteNumber
amount
currency
status
origin
line_items { description net vat gross rate kind }
}
}

line_items is the credit note’s amount split per VAT rate, the same rows REST returns as line_items. net + vat = gross on every row, and the rows add up to amount. rate is the fraction as stored ("0.2" for 20%), or null when the row carries no rate. A credit note stored before the split existed has an empty list.

query {
products(limit: 20) {
products {
id
name
description
sku
category
active
}
totalCount
}
}
query {
searchCustomers(q: "acme", limit: 10) {
id
name
email
companyName
lateFeesExempt
}
}

A customer no longer needs a wallet: wallet_address is nullable, and customers also carry companyName, billingAddress and lateFeesExempt. searchCustomers(q: "acme", limit: 8) matches name or email, case-insensitively, and createCustomer / updateCustomer take a CustomerInput. A customer needs an email or a wallet address. A duplicate email or wallet returns CUSTOMER_EMAIL_TAKEN or CUSTOMER_WALLET_TAKEN. updateCustomer does not change lateFeesExempt, which has its own mutation (see Late fees).

query {
estimates(status: sent, limit: 20) {
id
estimateNumber
revision
status
expiresOn
decision { decision signerName decidedAt }
}
estimatesCount(status: sent)
estimateStats { open openValueByCurrency acceptedThisMonth acceptanceRate expiringIn7Days }
}

estimate(id) returns one, estimates lists them with status, search, limit and offset (the dashboard pages them 10 or 20 at a time), estimatesCount is how many that same status and search match across every page (the total behind the page selector), and estimateStats returns the figures behind the dashboard’s Estimates page. An estimate’s decision is the decision on its current revision, and decisions is every revision’s decision. sentAt is the latest send, sends dates each sent revision in order, and voidedAt is set once the estimate is void. Mutations: createEstimate, updateEstimate (a sent estimate gets a new revision), sendEstimate, voidEstimate, convertEstimate and duplicateEstimate.

These operations serve the dashboard and are denied to API keys. Integrations use the REST API (/api/v1/estimates) and the SDKs. See Estimates.

query {
lateFeePolicy { enabled feeType amount currency graceDays repeat maxFees feeDue }
lateFeePreview(
input: { enabled: true, feeType: "percent", amount: "1.5", graceDays: 14,
repeat: "every_30_days", maxFees: 3, feeDue: "on_receipt" }
amount: "1000.00"
daysLate: 45
) { date amount }
}

lateFeePolicy is the saved policy, or null. lateFeePreview shows what a draft policy would charge, without saving anything. An invoice exposes lateFees (its ledger lines, removed ones included), lateFeeTerms (the sentence shown to the customer at send), lateFeeParentId (set on a fee invoice) and totalNowOwed (its balance plus its standing fee invoices).

invoiceById(id) returns one invoice for the dashboard drawer, so the costly late-fee fields load for one invoice instead of every row of a list. It is dashboard-only and denied to API keys, because its scoping follows the dashboard’s test or live mode rather than a key’s environment. API keys read a single invoice through REST.

query {
customerStatementCurrencies(customerId: "17")
customerStatement(
customerId: "17"
currency: "USD"
periodStart: "2026-09-01"
periodEnd: "2026-09-30"
) {
openingBalance
totalInvoiced
totalPayments
totalCredits
totalRefunds
closingBalance
isEmpty
openingDiffersFrom { number periodStart periodEnd closingBalance }
entries { date kind reference description charge credit balance invoiceId }
outstanding { invoiceId number issued due amountLeft daysOverdue payUrl }
ageing { current d1To30 d31To60 d61To90 d90Plus }
}
customerStatements(customerId: "17", limit: 20, offset: 0) {
total
items { id number currency periodStart periodEnd trigger status failureReason attempts recipients closingBalance sentAt }
}
}
OperationArgumentsReturns
customerStatementcustomerId: ID!, currency: String!, periodStart: Date!, periodEnd: Date!A CustomerStatement, built live from the ledger. An empty period is not an error: isEmpty is true
customerStatementCurrenciescustomerId: ID!The currencies the customer has activity in, as codes
customerStatementscustomerId: ID!, limit: Int = 20, offset: Int = 0A CustomerStatementRecordPage: items (newest first) and total. limit is capped at 200
statementSettingsnoneThe merchant’s StatementSettings. Dashboard-only

Date is a calendar day, YYYY-MM-DD, on the merchant’s calendar. periodEnd must be on or after periodStart. All money fields are strings holding exact decimals, never floats.

CustomerStatement fields:

FieldMeaning
customerId, currency, periodStart, periodEndWhat was asked for
testModeTrue when built from test-mode data
openingBalanceThe balance carried in from before periodStart
entriesThe activity, oldest first, each with date, kind (invoice, payment, credit_note or refund), reference, description, charge, credit, the running balance and the invoiceId it belongs to, if any
totalInvoiced, totalPayments, totalCredits, totalRefundsThe period’s totals
closingBalancePositive when the customer owes you, negative when they are in credit
outstandingInvoices still owed on periodEnd: invoiceId, number, issued, due, amountLeft, daysOverdue and payUrl (null when the invoice has no hosted page)
unrefundedOverpaymentOverpayments not yet refunded. closingBalance always equals the sum of outstanding.amountLeft minus this
ageingThe outstanding total by due date: current, d1To30, d31To60, d61To90, d90Plus
isEmptyTrue when there are no entries and the closing balance is zero
openingDiffersFrom{ number, periodStart, periodEnd, closingBalance } of the statement sent for the period ending the day before periodStart, set only when its closing balance is not this openingBalance (activity dated in that period was recorded after it was sent). Otherwise null

A statement shows up to 1,000 entries. Past that, customerStatement answers STATEMENT_TOO_LARGE: ask for a shorter period.

CustomerStatementRecord fields (one per emailed statement):

FieldMeaning
id, numberThe record’s id and its statement number, such as ST-0007
customerId, currency, periodStart, periodEndWhat it covered
triggerdashboard, api or schedule
statussending, sent or failed
failureReasonSet when status is failed. A scheduled send Plirin stopped retrying starts with not retried: and the reason, followed by ; last error: and the delivery error when there was one
attemptsDelivery attempts so far. A failed scheduled send is retried once, within 24 hours, so it has a retry left while this is below 2
recipientsThe addresses it went to: the customer, then any Cc
closingBalanceThe figure the customer was shown. Null unless status is sent
sentAt, createdAtTimestamps

StatementSettings fields:

FieldMeaning
modemanual or scheduled
frequencymonthly or weekly
dayOfMonth1 to 28, used by a monthly schedule
weekday0 (Monday) to 6 (Sunday), used by a weekly schedule
recipientsopen_balance or any_activity
defaultNoteText added to scheduled statements, up to 500 characters, or null
sendHourThe hour scheduled statements go out, shared with reminders, or null
nextRunOnThe next scheduled date, or null when manual or paused
scheduledUsedThisMonth, scheduledAllowanceThe month’s scheduled sends and the plan’s allowance (200 on Growth, 2,000 on Scale, 0 without the feature)
pausedThe stored mode is scheduled but the plan no longer includes statements, so nothing is sent

Customer.statementsOptOut is true when scheduled statements skip that customer. Set it with updateCustomer (see Mutations).

Access:

  • customerStatement, customerStatementCurrencies and customerStatements need the report.view permission and are available to API keys. They read the key’s own environment: a sk_test_ key sees test-mode statements, a sk_live_ key live ones.
  • statementSettings needs merchant.profile.manage and is denied to API keys. It can be read on a plan without statements (the allowance then reads 0, so the dashboard can show a locked state), but answers STATEMENTS_DISABLED while the platform switch is off.

The same data is available over REST (/api/v1) for integrations, with the SDKs wrapping these routes. Dates are from and to, as YYYY-MM-DD. Money is serialised as strings.

RouteReturns
GET /v1/customers/{id}/statement?currency=&from=&to=The statement as JSON, in snake_case (opening_balance, closing_balance, ageing.d1_30, and so on). currency, from and to are required
GET /v1/customers/{id}/statement/pdf?currency=&from=&to=The statement as application/pdf
GET /v1/customers/{id}/statements?limit=&offset={items, total} of sent statements. limit is 1 to 100, default 20
GET /v1/statements/{id}/pdfThe PDF of a sent statement, re-rendered from the figures that were sent. A statement that is sending or failed answers 409 STATEMENT_NOT_SENT

To send one, see POST /v1/customers/{id}/statement/send under Mutations. The errors are listed under Error codes.

Files attached to invoices and estimates. See Attachments for what they are, and Mutations for adding, sharing and removing them.

query {
invoiceById(id: "7b7b2f3c-0000-4000-8000-000000000001") {
id
attachments { id fileName contentType sizeBytes sharedWithCustomer createdAt }
}
attachmentDownloadUrl(id: "6f0c7f0e-9d2b-4b0e-8a47-3b3c1f6f3a10")
attachmentStorageUsage { attachmentBytes attachmentQuotaBytes }
}
  • Invoice.attachments and Estimate.attachments list the confirmed files of one document, oldest first. Select them on detail views only (invoiceById, estimate(id)), never on a list query: each one costs a lookup per row. They return an empty list while attachments are switched off.
  • attachmentDownloadUrl(id: ID!): String! returns a signed link to the file. It is valid for about a minute and forces a download, so ask for it when the user clicks, not when the page loads. The link is a credential: do not cache or log it.
  • attachmentStorageUsage returns the bytes you have used and your plan’s cap. Both are Float, because a 25 GB cap does not fit in a GraphQL Int. Usage is shared between test and live mode.
  • InvoiceAttachment.sizeBytes is an Int, which holds any single file (the limit is 20 MB).

These queries need only a signed-in session, like invoiceById, and return only what that session’s merchant owns.

Bills that other Plirin merchants sent to your account’s verified email, from senders you have accepted. See Bills Inbox for the rules behind what appears.

query {
receivedBills(
filter: { statuses: ["sent", "overdue"], dueFrom: "2026-10-01", dueTo: "2026-10-31", numberPrefix: "INV-", amount: "1200.50" }
first: 10
offset: 0
orderBy: { field: DUE, direction: ASC }
) {
items {
id
senderId
senderName
senderLegalName
invoiceNumber
amount
currency
amountRemaining
issuedAt
dueDate
status
hostedUrl
remindedAt
}
totalCount
asOf
}
receivedBillStats { open overdue reminded dueSoon }
}

receivedBills(filter: ReceivedBillFilter, first: Int = 10, offset: Int = 0, orderBy: ReceivedBillOrder): ReceivedBillPage! returns bills newest first unless orderBy says otherwise. first is 1 to 50 and offset is 0 or more; an offset past the end returns no items. totalCount is how many bills match the filter in all, whatever first and offset select, so a client can show page numbers. asOf is the server time the page was read. A bill that arrives while you page can shift later pages by one; totalCount shows it.

orderBy takes a field (SENDER, ISSUED, DUE or AMOUNT_REMAINING) and a direction (ASC or DESC). SENDER sorts by the sender’s name as the Bills list shows it: the verified legal name when there is one, otherwise the trading name, ignoring capitals. DUE puts bills with no due date last in either direction. AMOUNT_REMAINING sorts by the amount still to pay, the same figure amountRemaining returns, so a bill with nothing paid yet counts at its whole total and a void bill counts as 0. Bills that tie on the sort field come in a stable order, so no bill repeats or is skipped across pages.

ReceivedBill is a fixed list of fields and is deliberately not an Invoice: line items, notes and the sender’s other records are never reachable through it. amount is the bill’s total (subtotal plus tax and tip, less any discount), the figure the bill asks for; amountRemaining is the amount still to pay (the total less what has been paid; the whole total while nothing is paid; 0 for a void bill, which owes nothing); amount and amountRemaining are decimal strings. dueDate is a YYYY-MM-DD day on the sender’s calendar, the day printed on the bill. issuedAt is an ISO 8601 instant. remindedAt is an ISO 8601 instant too: when the sender last reminded this account about the bill by email. It is null if they never did, and null once the bill can no longer be paid (paid or void), so it is never set on a bill that is not owed. Only a reminder whose email went out sets it, and the first email about an invoice, which is the invoice itself, is not a reminder. hostedUrl is the bill’s hosted page, where it can be paid. senderLegalName is the sender’s KYB-verified legal name, or null.

ReceivedBillFilter fields are all optional and combine with AND:

FieldTypeMatches
senderIdIDOne sender
statuses[String!]Any of the listed statuses (sent, pending, paid, overdue, void), lowercase as status returns them. Any other value, including draft, is an error
dueFrom / dueToStringBills due on those days or between them, YYYY-MM-DD, inclusive, on the sender’s calendar. dueTo must not be before dueFrom
numberPrefixStringInvoice numbers that start with the value (no substring search)
amountDecimalExactly this bill total (tax, tip and discount applied, as in amount)
remindedBooleantrue lists only bills the sender has reminded you about that are still owed (sent or overdue): the set receivedBillStats.reminded counts. It combines with statuses, so reminded: true with statuses: ["overdue"] is the overdue bills the sender has chased. false or omitted does not narrow the list
searchStringBills whose sender name, sender KYB legal name, invoice number or description contains the text anywhere, ignoring case. % and _ are taken literally
query {
pendingSenders { senderId name legalName billCount }
networkSenders { senderId name legalName status decidedAt }
inboxNews { newBills newPendingSenders latest { id senderName amount currency dueDate } }
receivedBillStats { open overdue reminded dueSoon }
}

pendingSenders lists senders that have billed your email and that you have neither accepted nor blocked. It is computed from the bills on every call and never stored. inboxNews is what arrived since the signed-in person last marked the inbox seen: newBills counts bills from accepted senders that are new or have been reminded about while still owed (a bill counts once, however many events it had), newPendingSenders counts senders awaiting an answer that sent something new, and latest is the accepted bill with the most recent event. It is kept for older clients; the dashboard reads navActivity, which also says whether latest is a reminder. “Last marked seen” is per person (the owner and each staff member have their own). Before a person has marked the inbox seen for the first time, it counts from the release for anyone who was already a member (owner or staff), except an owner who had already opened Bills, who keeps that earlier time; a member who joins later counts from their account’s creation. So it is not zero by default and a first bill does count. Neither query writes anything.

receivedBillStats returns four counts for the Bills page summary strip, over all of your received bills and independent of any filter or search the list uses (it takes no arguments): open (status sent or overdue), overdue, reminded (the bills reminded: true lists) and dueSoon (sent bills whose due date falls from today through the next 7 days, inclusive, today being the date in your account’s timezone). They are counts only, never amounts: bills arrive in different currencies and a sum across currencies would be a wrong number. It follows the same inbox rules as the list, and returns zeros while the Network Inbox switch is off.

networkSenders lists the senders you have already decided about, ordered by name, with status ACCEPTED or BLOCKED and decidedAt (when the decision last changed). It is how an accepted sender is blocked later and a blocked one unblocked: pass the senderId to decideSender with BLOCK or ACCEPT. Either works whether or not that sender has a bill to show right now. It only ever lists your own decisions. legalName is the sender’s KYB-verified legal name, or null.

canSaveBill(slug: String!): Boolean! answers whether saveBillToInbox would succeed for the bill with that public slug: a live bill with a hosted page, sent to your verified email, by another merchant. The hosted bill page asks it before showing Save to my Plirin inbox, so a sender never sees the button on their own bill. Every other case (an unknown slug, a draft or test bill, another address, your own bill, the switch off) is the same false; it reveals nothing beyond the answer.

When the platform’s Network Inbox switch is off, the list queries return empty results (no bills, no senders, zero counts) rather than an error. An account whose email is not verified also gets empty results. Invalid input (first out of range, a negative offset, a malformed filter, a backwards date range) is a client error.

Customer.inNetworkInbox: Boolean! is true when that customer’s Plirin account has accepted you as a sender. It is false for a customer who is not a member, whose email is unverified, who has not decided, or who blocked you: a block is never revealed. It is also false while the Network Inbox switch is off. Selecting it on customers costs one extra query for the whole page.

Invoice.customerInNetworkInbox: Boolean! is the same rule for one invoice: true only for a live invoice whose customer’s email receives your bills in a Plirin inbox, so a reminder about it will also show there. It is false for a draft or test-mode invoice, for a blocked customer (a block is never revealed), and while the switch is off. The sendInvoiceReminder mutation returns it, which is how the dashboard decides whether its confirmation says “They’ll also see it in their Plirin inbox”. Unlike the inbox operations above, it can be selected with an API key: it tells you only what Customer.inNetworkInbox already does.

The counts in the dashboard sidebar and the toasts that go with them. See Activity badges for what each one means.

query {
navActivity {
asOf
enabled
invoicesPaid
estimatesAnswered
paymentsReceived
refundsAwaitingApproval
newBills
newBillSenders
latestBill { id senderName amount amountRemaining currency dueDate }
latestBillReminded
}
navNewItems(page: BILLS) { items { id label } more }
}
mutation {
markPageSeen(page: BILLS, asOf: "2026-10-11T09:30:00Z")
}

navActivity returns every badge count for the signed-in person in one read, each 0 when the person lacks the permission or feature behind that page. enabled is false when Plirin has switched the badges off, and then every count is 0. asOf is the server time of the read, taken before counting: pass it back to markPageSeen.

newBills counts bills that are new or reminded since this person last looked at Bills, once each. latestBill is the one the toast names, the accepted bill with the most recent event, and latestBillReminded is true when it is there because its sender reminded this person about a bill they already knew, and false when the bill is new (a new bill that was also reminded in the same window is still new), or when latestBill is null. The toast reads “Reminder from Acme Co, $1,200.00 due 15 Oct 2026” for a reminder, using amountRemaining, and says “due” even when the date has passed.

navNewItems(page: NavPage!) lists what a badge counted, newest first, as items { id label } and more (how many are not listed). page is INVOICES, ESTIMATES or BILLS. For BILLS it lists only the bills reminded since the person last looked (accepted senders, still owed), at most five (more counts the rest), and label is the sender’s name as the Bills list shows it (the verified legal name when there is one), then the invoice number, such as Acme Co INV-0012, and just the name when the bill has no number; bills that are new are not listed, because the new-bill toast already names them. It is empty while the network inbox or the badges are switched off.

markPageSeen(page: NavPage!, asOf: DateTime): Boolean! records that the person looked at a page, up to asOf. page is INVOICES, ESTIMATES, TRANSACTIONS or BILLS. For BILLS it writes the same per-person mark as markInboxSeen, so reminders are cleared along with new bills. A future asOf is treated as now and the mark never moves backwards. It returns false and writes nothing when the badges are switched off or the person cannot see that page’s badge. Send it only for the default, unfiltered first page of the list, so a filtered view can never hide something new.

query {
tokenBalances(
blockchain: "solana-devnet"
walletAddress: "your-wallet-address"
tokenSymbols: ["USDC", "USDT"]
) {
tokenSymbol
balance
}
}
query {
salesSummaryReport(timeframe: MONTHLY) {
totalSales
transactionCount
averageTransactionValue
reportPeriod
}
}

What each country calls its tax, and the country list for a picker. Both are the same in test and live mode and are callable with an API key.

query {
taxRegimes {
countryCode # "AU"
type # vat | gst | sales_tax
taxName # "GST"
taxIdLabel # "ABN"
itemizedTax # true: per-line rates are offered
invoiceTitle # "Tax Invoice", when the country expects it
}
countries { code name aliases }
}

The merchant’s own resolved wording is on me. Clients should read these rather than derive labels themselves:

query {
me {
... on Merchant {
countryCode
taxId
taxNameOverride
taxIdLabelOverride
taxLabels { name idLabel invoiceTitle itemizedOffered regimeType }
}
}
}

Invoices expose taxLabel, taxIdLabel and taxId: the values stored when the invoice was issued, or the merchant’s current settings for a draft.

Per-line (itemized) tax rates. vatRates, vatSummary and vatReport are dashboard-only — see Authentication — they are rejected for API-key requests.

query {
vatRates {
id
name
rate
kind
isDefault
isActive
exemptionNote
}
}
query {
vatSummary(startDate: "2026-01-01", endDate: "2026-03-31") {
rate
kind
netTotal
vatTotal
grossTotal
}
}

vatReport — the summary plus what it excludes

Section titled “vatReport — the summary plus what it excludes”

vatSummary above returns only the rows, and scopes them to your account’s default currency, because a VAT return is filed in a single currency.

That scoping is correct but invisible: if you issued invoices in another currency during the period, they are not in those totals and nothing in the response says so. vatReport returns the same rows alongside the currency they are stated in and a count of the issued invoices left out, per currency.

query {
vatReport(startDate: "2026-01-01", endDate: "2026-03-31") {
currency
rows {
rate
kind
netTotal
vatTotal
grossTotal
}
excludedCurrencies {
currency
invoiceCount
}
}
}

excludedCurrencies is empty for a domestic-only account, which is the common case. When it is not empty, the totals are still correct for currency — they are simply not your whole story for that period.

It reports counts, not converted amounts, deliberately. Converting a foreign invoice into your filing currency requires an FX rate and a rate date, and which rate applies is a determination your tax authority prescribes. Reporting a converted figure here would replace a visible omission with an invisible wrong number on a document you file.

vatSummary is unchanged and still supported.

exportReconciliationCSV matches on-chain settlement against your ledger. It is the query for closing the books: it carries the columns that expose disagreement, rather than repeating the invoice list.

query {
exportReconciliationCSV(dateFrom: "2026-01-01", dateTo: "2026-03-31") {
downloadUrl
rowCount
generatedAt
totals {
state
count
}
}
}

downloadUrl is a data:text/csv;base64,... URL you can save directly. Each row is classified into one of four states:

StateMeaning
matchedConfirmed, and the observed on-chain amount agrees with the ledger
mismatchConfirmed, but the amounts disagree — mismatch_reason says how (e.g. underpaid)
unsettledSomething was observed on-chain, but the payment is not confirmed yet
unrecordedNothing observed on-chain for this transaction

totals gives the count per state, so you can see at a glance whether a period reconciles before opening the file.

The CSV carries 19 columns including observed_amount, mismatch_reason, settlement_seen_at, confirmed_at, chain, platform_fee and total_refunded. It is scoped to your current environment: a test payment never lands in a live reconciliation.