Introduction
The Solarium API is a GraphQL API that gives you full programmatic access to your accounting data: transactions, accounts, contacts, receipts, and financial reports.
All requests go to a single endpoint. Authentication is via API key (Bearer token) or JWT. Every query and mutation requires an organizationId. Your API key is scoped to one organization, so use the myOrganizations query to resolve it.
Base URL: https://solarium-api.lovemaple.app/graphql
Set Up Your AI Agent
The fastest way to get started: copy the instructions below and paste them into Claude, ChatGPT, or any AI agent. Replace sol_your_key_here with your API key from Settings > API Keys. When you create a key in the dashboard, we'll pre-fill these instructions with your actual key.
Quickstart (Manual)
If you're writing code directly, grab your API key and make your first request:
# ━━━ AI Agent Instructions (copy & paste into Claude/ChatGPT) ━━━
#
# You have access to the Solarium accounting API.
#
# Endpoint: https://solarium-api.lovemaple.app/graphql
# Method: POST with JSON body {"query": "...", "variables": {...}}
# Auth header: Authorization: Bearer sol_your_key_here
#
# First, resolve your org ID:
# query { myOrganizations { id name } }
# Use the returned id as organizationId on every call.
#
# Transaction lifecycle: create → classify → confirm
#
# Key operations:
# listLedgerEvents(organizationId, limit, status, search, dateFrom, dateTo)
# createLedgerEvent(organizationId, merchantName, amount, transactionDate, eventType)
# updateStagedEntry(organizationId, eventId, categoryId, paymentId)
# confirmLedgerEvent(organizationId, eventId)
# listAccounts(organizationId, limit, search)
# listContacts(organizationId, limit, search)
# incomeStatement(organizationId, startDate, endDate)
# importBankStatement(organizationId, paymentAccountId, rows: [...])
#
# Amounts are strings ("42.50"). Dates are YYYY-MM-DD.
# Event types: "expense", "income", "transfer".
# Statuses: "needs_review", "confirmed", "synced", "reconciled".
# See full docs at: https://trysolarium.com/integrations/api
# ━━━ Manual Quickstart ━━━
# curl: resolve your org ID
curl -X POST https://solarium-api.lovemaple.app/graphql \
-H "Authorization: Bearer sol_your_key_here" \
-H "Content-Type: application/json" \
-d '{"query": "{ myOrganizations { id name } }"}'
# Python: full example
import httpx
API_URL = "https://solarium-api.lovemaple.app/graphql"
API_KEY = "sol_your_key_here"
def query(q, variables=None):
resp = httpx.post(
API_URL,
json={"query": q, "variables": variables or {}},
headers={"Authorization": f"Bearer {API_KEY}"},
)
return resp.json()["data"]
# 1. Get your org ID
orgs = query("{ myOrganizations { id name } }")
org_id = orgs["myOrganizations"][0]["id"]
# 2. List accounts
accounts = query("""
query($orgId: UUID!) {
listAccounts(organizationId: $orgId, limit: 10) {
items { id name accountType currentBalance }
totalCount
}
}
""", {"orgId": org_id})
# 3. Create a transaction
result = query("""
mutation($orgId: UUID!) {
createLedgerEvent(
organizationId: $orgId
merchantName: "Staples Canada"
amount: "42.50"
transactionDate: "2026-01-15"
eventType: "expense"
) { success eventId }
}
""", {"orgId": org_id})
# 4. Classify it (set category + payment account)
query("""
mutation($orgId: UUID!, $eventId: UUID!) {
updateStagedEntry(
organizationId: $orgId
eventId: $eventId
categoryId: "category-account-uuid"
paymentId: "payment-account-uuid"
) { success }
}
""", {"orgId": org_id, "eventId": result["createLedgerEvent"]["eventId"]})
# 5. Confirm it (posts to books)
query("""
mutation($orgId: UUID!, $eventId: UUID!) {
confirmLedgerEvent(
organizationId: $orgId
eventId: $eventId
) { success journalEntryId }
}
""", {"orgId": org_id, "eventId": result["createLedgerEvent"]["eventId"]}){
"data": {
"myOrganizations": [
{
"id": "a1b2c3d4-...",
"name": "My Company Inc."
}
]
}
}# ━━━ AI Agent Instructions (copy & paste into Claude/ChatGPT) ━━━
#
# You have access to the Solarium accounting API.
#
# Endpoint: https://solarium-api.lovemaple.app/graphql
# Method: POST with JSON body {"query": "...", "variables": {...}}
# Auth header: Authorization: Bearer sol_your_key_here
#
# First, resolve your org ID:
# query { myOrganizations { id name } }
# Use the returned id as organizationId on every call.
#
# Transaction lifecycle: create → classify → confirm
#
# Key operations:
# listLedgerEvents(organizationId, limit, status, search, dateFrom, dateTo)
# createLedgerEvent(organizationId, merchantName, amount, transactionDate, eventType)
# updateStagedEntry(organizationId, eventId, categoryId, paymentId)
# confirmLedgerEvent(organizationId, eventId)
# listAccounts(organizationId, limit, search)
# listContacts(organizationId, limit, search)
# incomeStatement(organizationId, startDate, endDate)
# importBankStatement(organizationId, paymentAccountId, rows: [...])
#
# Amounts are strings ("42.50"). Dates are YYYY-MM-DD.
# Event types: "expense", "income", "transfer".
# Statuses: "needs_review", "confirmed", "synced", "reconciled".
# See full docs at: https://trysolarium.com/integrations/api
# ━━━ Manual Quickstart ━━━
# curl: resolve your org ID
curl -X POST https://solarium-api.lovemaple.app/graphql \
-H "Authorization: Bearer sol_your_key_here" \
-H "Content-Type: application/json" \
-d '{"query": "{ myOrganizations { id name } }"}'
# Python: full example
import httpx
API_URL = "https://solarium-api.lovemaple.app/graphql"
API_KEY = "sol_your_key_here"
def query(q, variables=None):
resp = httpx.post(
API_URL,
json={"query": q, "variables": variables or {}},
headers={"Authorization": f"Bearer {API_KEY}"},
)
return resp.json()["data"]
# 1. Get your org ID
orgs = query("{ myOrganizations { id name } }")
org_id = orgs["myOrganizations"][0]["id"]
# 2. List accounts
accounts = query("""
query($orgId: UUID!) {
listAccounts(organizationId: $orgId, limit: 10) {
items { id name accountType currentBalance }
totalCount
}
}
""", {"orgId": org_id})
# 3. Create a transaction
result = query("""
mutation($orgId: UUID!) {
createLedgerEvent(
organizationId: $orgId
merchantName: "Staples Canada"
amount: "42.50"
transactionDate: "2026-01-15"
eventType: "expense"
) { success eventId }
}
""", {"orgId": org_id})
# 4. Classify it (set category + payment account)
query("""
mutation($orgId: UUID!, $eventId: UUID!) {
updateStagedEntry(
organizationId: $orgId
eventId: $eventId
categoryId: "category-account-uuid"
paymentId: "payment-account-uuid"
) { success }
}
""", {"orgId": org_id, "eventId": result["createLedgerEvent"]["eventId"]})
# 5. Confirm it (posts to books)
query("""
mutation($orgId: UUID!, $eventId: UUID!) {
confirmLedgerEvent(
organizationId: $orgId
eventId: $eventId
) { success journalEntryId }
}
""", {"orgId": org_id, "eventId": result["createLedgerEvent"]["eventId"]}){
"data": {
"myOrganizations": [
{
"id": "a1b2c3d4-...",
"name": "My Company Inc."
}
]
}
}Authentication
Authenticate by passing your API key as a Bearer token in the Authorization header.
API keys are created in the Solarium dashboard at Settings > API Keys. API keys start with sol_ and are shown once at creation. Store them securely. Only the SHA-256 hash is stored on our end.
API keys are scoped to a single organization. You cannot access another organization's data with an API key. Admin operations (managing members, creating other API keys) are not available via API key.
Rate Limiting
API key requests are limited to 5,000 requests per hour per API key. The limit resets on a rolling 1-hour window from the first request.
Every API key response includes rate limit headers:
X-RateLimit-Limit: your hourly quota (5000)X-RateLimit-Remaining: requests left in the current window
If you exceed the limit, requests return 429 Too Many Requests with a Retry-After header. JWT-authenticated requests (dashboard sessions) are not rate limited.
# API Key auth (recommended for integrations)
curl -X POST https://solarium-api.lovemaple.app/graphql \
-H "Authorization: Bearer sol_abc123..." \
-H "Content-Type: application/json" \
-d '{"query": "{ myOrganizations { id name } }"}'
# JWT auth (for user sessions)
curl -X POST https://solarium-api.lovemaple.app/graphql \
-H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"query": "{ myOrganizations { id name } }"}'# Unauthenticated request
{
"errors": [
{ "message": "Authentication Required." }
]
}
# Wrong Organization
{
"errors": [
{ "message": "API Key Not Authorized For This Organization." }
]
}
# Rate limit exceeded (429)
{
"errors": [
{ "message": "Rate Limit Exceeded. 5,000 Requests/Hour." }
]
}
# Headers: Retry-After: 60# API Key auth (recommended for integrations)
curl -X POST https://solarium-api.lovemaple.app/graphql \
-H "Authorization: Bearer sol_abc123..." \
-H "Content-Type: application/json" \
-d '{"query": "{ myOrganizations { id name } }"}'
# JWT auth (for user sessions)
curl -X POST https://solarium-api.lovemaple.app/graphql \
-H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"query": "{ myOrganizations { id name } }"}'# Unauthenticated request
{
"errors": [
{ "message": "Authentication Required." }
]
}
# Wrong Organization
{
"errors": [
{ "message": "API Key Not Authorized For This Organization." }
]
}
# Rate limit exceeded (429)
{
"errors": [
{ "message": "Rate Limit Exceeded. 5,000 Requests/Hour." }
]
}
# Headers: Retry-After: 60Errors
The Solarium API returns errors in a consistent JSON shape. GraphQL requests always return 200 OK at the HTTP level. Errors appear in the errors array of the response body.
The only exceptions are 429 Too Many Requests (rate limit) and 400 Bad Request (malformed JSON or invalid GraphQL syntax), which return non-200 HTTP status codes.
Error Object
Each error has a message string. Some errors include a path array indicating which field triggered the error.
Common Errors
| Message | Cause |
|---|---|
Authentication Required. | No token or API key provided |
API Key Not Authorized For This Organization. | API key belongs to a different organization |
Organization Not Found. | Invalid organizationId |
Not A Member Of This Organization. | JWT user is not a member of this organization |
API Keys Cannot Perform Admin Operations. | API key used for an admin-only mutation (e.g. managing members) |
Must Be Owner Or Admin. | Insufficient role for this operation |
Rate Limit Exceeded. 5,000 Requests/Hour. | API key exceeded hourly quota (HTTP 429) |
Mutation Errors
Mutations return a success boolean and an error string instead of throwing. A failed mutation returns success: false with a human-readable error. The HTTP status is still 200.
# Successful mutation
mutation {
createAccount(
organizationId: "a1b2c3d4-..."
name: "Office Supplies"
accountType: "expense"
classification: "expense"
) {
account { id name }
success
error
}
}# GraphQL-level error (in errors array)
{
"errors": [
{
"message": "Authentication Required.",
"path": ["createAccount"]
}
],
"data": { "createAccount": null }
}
# Mutation-level error (success: false)
{
"data": {
"createAccount": {
"account": null,
"success": false,
"error": "Account with this name already exists."
}
}
}
# Rate limit error (HTTP 429)
{
"errors": [
{ "message": "Rate Limit Exceeded. 5,000 Requests/Hour." }
]
}# Successful mutation
mutation {
createAccount(
organizationId: "a1b2c3d4-..."
name: "Office Supplies"
accountType: "expense"
classification: "expense"
) {
account { id name }
success
error
}
}# GraphQL-level error (in errors array)
{
"errors": [
{
"message": "Authentication Required.",
"path": ["createAccount"]
}
],
"data": { "createAccount": null }
}
# Mutation-level error (success: false)
{
"data": {
"createAccount": {
"account": null,
"success": false,
"error": "Account with this name already exists."
}
}
}
# Rate limit error (HTTP 429)
{
"errors": [
{ "message": "Rate Limit Exceeded. 5,000 Requests/Hour." }
]
}Pagination
All list endpoints use offset-based pagination with a consistent interface.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | Int | 25 | Number of items per page (max 200) |
offset | Int | 0 | Number of items to skip |
search | String | — | Case-insensitive text filter (searches relevant fields like name, memo, etc.) |
sortBy | String | — | Field to sort by (e.g. name, currentBalance, txnDate) |
sortDir | String | asc | Sort direction: asc or desc |
Response shape
Every paginated response includes:
items: the array of resultstotalCount: total number of matching records (before pagination)pageSize: thelimitthat was appliedoffset: theoffsetthat was applied
To check if there are more pages: offset + items.length < totalCount.
Requesting beyond the end of the data returns an empty items array, not an error.
# Page 1: first 10 accounts
query {
listAccounts(
organizationId: "a1b2c3d4-..."
limit: 10
offset: 0
sortBy: "name"
sortDir: "asc"
) {
items { id name currentBalance }
totalCount
pageSize
offset
}
}
# Page 2: next 10
query {
listAccounts(
organizationId: "a1b2c3d4-..."
limit: 10
offset: 10
sortBy: "name"
sortDir: "asc"
) {
items { id name currentBalance }
totalCount
pageSize
offset
}
}# Page 1 response
{
"data": {
"listAccounts": {
"items": [
{ "id": "...", "name": "Accounts Payable", "currentBalance": 4200.00 },
{ "id": "...", "name": "Accounts Receivable", "currentBalance": 12400.00 }
],
"totalCount": 24,
"pageSize": 10,
"offset": 0
}
}
}
# Last page (fewer items than limit)
{
"data": {
"listAccounts": {
"items": [
{ "id": "...", "name": "Travel", "currentBalance": 890.00 }
],
"totalCount": 24,
"pageSize": 10,
"offset": 20
}
}
}# Page 1: first 10 accounts
query {
listAccounts(
organizationId: "a1b2c3d4-..."
limit: 10
offset: 0
sortBy: "name"
sortDir: "asc"
) {
items { id name currentBalance }
totalCount
pageSize
offset
}
}
# Page 2: next 10
query {
listAccounts(
organizationId: "a1b2c3d4-..."
limit: 10
offset: 10
sortBy: "name"
sortDir: "asc"
) {
items { id name currentBalance }
totalCount
pageSize
offset
}
}# Page 1 response
{
"data": {
"listAccounts": {
"items": [
{ "id": "...", "name": "Accounts Payable", "currentBalance": 4200.00 },
{ "id": "...", "name": "Accounts Receivable", "currentBalance": 12400.00 }
],
"totalCount": 24,
"pageSize": 10,
"offset": 0
}
}
}
# Last page (fewer items than limit)
{
"data": {
"listAccounts": {
"items": [
{ "id": "...", "name": "Travel", "currentBalance": 890.00 }
],
"totalCount": 24,
"pageSize": 10,
"offset": 20
}
}
}Transactions
Transactions (LedgerEvents) are the core entity in Solarium. Each transaction flows through a lifecycle: created → classified → confirmed → synced. Use createLedgerEvent for manual entries, or import via bank statement. Classify with updateStagedEntry (set category + payment account), then confirm to post to the books.
List Transactions
Returns a paginated, filterable list of ledger events. Supports status filtering, date ranges, amount ranges, account/category scoping, and full-text search.
Parameters
organizationIdUUID!requiredOrganization ID
status[String]Filter by status (needs_review, confirmed, synced, reconciled, pending, ignored)
hasDocumentBooleanFilter by whether a receipt/document is attached
sinceDaysIntOnly return events from the last N days
dateFromStringStart date filter (YYYY-MM-DD)
dateToStringEnd date filter (YYYY-MM-DD)
amountMinStringMinimum amount filter (inclusive)
amountMaxStringMaximum amount filter (inclusive)
accountIds[UUID]Filter by expense/income account IDs
categoryIds[UUID]Filter by category account IDs
searchStringFull-text search on merchant name and memo
limitIntPage size (default 25, max 200)
offsetIntNumber of records to skip (default 0)
sortByStringField to sort by (e.g. transactionDate, functionalAmount)
sortDirStringasc or desc (default desc)
query {
listLedgerEvents(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
status: ["needs_review"]
dateFrom: "2026-01-01"
dateTo: "2026-06-30"
limit: 10
) {
items {
id
eventType
merchantName
functionalAmount
functionalCurrency
transactionDate
status
hasDocument
account { id name }
paymentAccount { id name }
}
totalCount
pageSize
offset
}
}{
"data": {
"listLedgerEvents": {
"items": [
{
"id": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"eventType": "expense",
"merchantName": "Staples Canada",
"functionalAmount": "127.43",
"functionalCurrency": "CAD",
"transactionDate": "2026-06-18",
"status": "needs_review",
"hasDocument": true,
"account": {
"id": "d3e4f5a6-b7c8-9012-def0-123456789abc",
"name": "Office Supplies"
},
"paymentAccount": {
"id": "e4f5a6b7-c8d9-0123-ef01-23456789abcd",
"name": "TD Visa *8821"
}
},
{
"id": "c8d9e0f1-a2b3-4567-c890-def012345678",
"eventType": "expense",
"merchantName": "Tim Hortons",
"functionalAmount": "14.85",
"functionalCurrency": "CAD",
"transactionDate": "2026-06-17",
"status": "needs_review",
"hasDocument": false,
"account": null,
"paymentAccount": {
"id": "e4f5a6b7-c8d9-0123-ef01-23456789abcd",
"name": "TD Visa *8821"
}
}
],
"totalCount": 43,
"pageSize": 10,
"offset": 0
}
}
}Get a Transaction
Returns the full detail for a single ledger event, including attached receipts, journal entry, and classification data.
Parameters
organizationIdUUID!requiredOrganization ID
eventIdUUID!requiredLedger event ID
query {
getLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
) {
id
eventType
merchantName
functionalAmount
functionalCurrency
transactionDate
status
memo
hasDocument
account { id name accountType }
paymentAccount { id name }
category { id name }
journalEntry { id txnDate totalAmount }
receipts { id filename s3Key uploadedAt }
}
}{
"data": {
"getLedgerEvent": {
"id": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"eventType": "expense",
"merchantName": "Staples Canada",
"functionalAmount": "127.43",
"functionalCurrency": "CAD",
"transactionDate": "2026-06-18",
"status": "needs_review",
"memo": "Printer paper and toner cartridges",
"hasDocument": true,
"account": {
"id": "d3e4f5a6-b7c8-9012-def0-123456789abc",
"name": "Office Supplies",
"accountType": "expense"
},
"paymentAccount": {
"id": "e4f5a6b7-c8d9-0123-ef01-23456789abcd",
"name": "TD Visa *8821"
},
"category": {
"id": "d3e4f5a6-b7c8-9012-def0-123456789abc",
"name": "Office Supplies"
},
"journalEntry": null,
"receipts": [
{
"id": "f0a1b2c3-d4e5-6789-a012-bcdef0123456",
"filename": "staples-receipt-jun18.pdf",
"s3Key": "receipts/a1b2c3d4/2026/06/staples-receipt-jun18.pdf",
"uploadedAt": "2026-06-18T14:32:00Z"
}
]
}
}
}Create a Transaction
Creates a new ledger event (manual entry). The event starts in needs_review status and must be classified and confirmed before it posts to the books.
Parameters
organizationIdUUID!requiredOrganization ID
merchantNameString!requiredVendor / merchant name
amountString!requiredTransaction amount (positive decimal string)
transactionDateString!requiredDate of transaction (YYYY-MM-DD)
eventTypeStringexpense, income, or transfer (default expense)
categoryIdUUIDExpense/income account to categorize under
paymentAccountIdUUIDBank/credit card account used for payment
mutation {
createLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
merchantName: "Canada Post"
amount: "34.95"
transactionDate: "2026-06-20"
eventType: "expense"
categoryId: "a6b7c8d9-e0f1-2345-6789-abcdef012345"
paymentAccountId: "e4f5a6b7-c8d9-0123-ef01-23456789abcd"
) {
success
eventId
}
}{
"data": {
"createLedgerEvent": {
"success": true,
"eventId": "d9e0f1a2-b3c4-5678-d901-ef0123456789"
}
}
}Update a Transaction
Updates mutable fields on an existing ledger event. Only events in needs_review or pending status can be updated.
Parameters
organizationIdUUID!requiredOrganization ID
eventIdUUID!requiredLedger event ID
transactionDateStringUpdated transaction date (YYYY-MM-DD)
memoStringUpdated memo / description
amountStringUpdated amount (positive decimal string)
eventTypeStringUpdated type: expense, income, or transfer
mutation {
updateLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
memo: "Office supplies — printer paper and toner"
amount: "127.43"
) {
success
event {
id
memo
functionalAmount
status
}
}
}{
"data": {
"updateLedgerEvent": {
"success": true,
"event": {
"id": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"memo": "Office supplies — printer paper and toner",
"functionalAmount": "127.43",
"status": "needs_review"
}
}
}
}Confirm a Transaction
Confirms a classified ledger event, posting it to the books by creating a journal entry. The event must have a category and payment account assigned before confirming.
Parameters
organizationIdUUID!requiredOrganization ID
eventIdUUID!requiredLedger event ID to confirm
mutation {
confirmLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
) {
success
eventId
journalEntryId
}
}{
"data": {
"confirmLedgerEvent": {
"success": true,
"eventId": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"journalEntryId": "a2b3c4d5-e6f7-8901-ab23-cdef45678901"
}
}
}Reject a Transaction
Rejects a ledger event, moving it to ignored status. Rejected events are excluded from the books and review queue.
Parameters
organizationIdUUID!requiredOrganization ID
eventIdUUID!requiredLedger event ID to reject
mutation {
rejectLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "c8d9e0f1-a2b3-4567-c890-def012345678"
) {
success
eventId
}
}{
"data": {
"rejectLedgerEvent": {
"success": true,
"eventId": "c8d9e0f1-a2b3-4567-c890-def012345678"
}
}
}Classify Transaction
Assigns classification data to a staged ledger event: category (expense/income account), payment account, and optional tax breakdown. This is the primary way to prepare a transaction for confirmation.
Parameters
organizationIdUUID!requiredOrganization ID
eventIdUUID!requiredLedger event ID to classify
categoryIdUUIDExpense or income account ID
paymentIdUUIDBank/credit card payment account ID
taxAmountStringTax portion of the amount (e.g. HST/GST)
taxAccountIdUUIDTax liability account ID (e.g. HST Payable)
taxCodeStringTax code identifier (e.g. HST_ON, GST, exempt)
mutation {
updateStagedEntry(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "c8d9e0f1-a2b3-4567-c890-def012345678"
categoryId: "f5a6b7c8-d9e0-1234-5678-9abcdef01234"
paymentId: "e4f5a6b7-c8d9-0123-ef01-23456789abcd"
taxAmount: "1.93"
taxAccountId: "a0b1c2d3-e4f5-6789-0abc-def123456789"
taxCode: "HST_ON"
) {
success
}
}{
"data": {
"updateStagedEntry": {
"success": true
}
}
}Attach Receipt to Transaction
Attaches an uploaded receipt (image or PDF) to a ledger event. The file must already be uploaded to S3 — pass the s3Key returned from the upload endpoint.
Parameters
organizationIdUUID!requiredOrganization ID
eventIdUUID!requiredLedger event ID to attach the receipt to
s3KeyString!requiredS3 object key of the uploaded receipt file
filenameStringOriginal filename for display (e.g. receipt.pdf)
mutation {
attachReceiptToEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
s3Key: "receipts/a1b2c3d4/2026/06/canadapost-shipping-label.pdf"
filename: "canadapost-shipping-label.pdf"
) {
success
receiptId
}
}{
"data": {
"attachReceiptToEvent": {
"success": true,
"receiptId": "b1c2d3e4-f5a6-7890-bcde-f01234567890"
}
}
}query {
listLedgerEvents(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
status: ["needs_review"]
dateFrom: "2026-01-01"
dateTo: "2026-06-30"
limit: 10
) {
items {
id
eventType
merchantName
functionalAmount
functionalCurrency
transactionDate
status
hasDocument
account { id name }
paymentAccount { id name }
}
totalCount
pageSize
offset
}
}{
"data": {
"listLedgerEvents": {
"items": [
{
"id": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"eventType": "expense",
"merchantName": "Staples Canada",
"functionalAmount": "127.43",
"functionalCurrency": "CAD",
"transactionDate": "2026-06-18",
"status": "needs_review",
"hasDocument": true,
"account": {
"id": "d3e4f5a6-b7c8-9012-def0-123456789abc",
"name": "Office Supplies"
},
"paymentAccount": {
"id": "e4f5a6b7-c8d9-0123-ef01-23456789abcd",
"name": "TD Visa *8821"
}
},
{
"id": "c8d9e0f1-a2b3-4567-c890-def012345678",
"eventType": "expense",
"merchantName": "Tim Hortons",
"functionalAmount": "14.85",
"functionalCurrency": "CAD",
"transactionDate": "2026-06-17",
"status": "needs_review",
"hasDocument": false,
"account": null,
"paymentAccount": {
"id": "e4f5a6b7-c8d9-0123-ef01-23456789abcd",
"name": "TD Visa *8821"
}
}
],
"totalCount": 43,
"pageSize": 10,
"offset": 0
}
}
}query {
getLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
) {
id
eventType
merchantName
functionalAmount
functionalCurrency
transactionDate
status
memo
hasDocument
account { id name accountType }
paymentAccount { id name }
category { id name }
journalEntry { id txnDate totalAmount }
receipts { id filename s3Key uploadedAt }
}
}{
"data": {
"getLedgerEvent": {
"id": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"eventType": "expense",
"merchantName": "Staples Canada",
"functionalAmount": "127.43",
"functionalCurrency": "CAD",
"transactionDate": "2026-06-18",
"status": "needs_review",
"memo": "Printer paper and toner cartridges",
"hasDocument": true,
"account": {
"id": "d3e4f5a6-b7c8-9012-def0-123456789abc",
"name": "Office Supplies",
"accountType": "expense"
},
"paymentAccount": {
"id": "e4f5a6b7-c8d9-0123-ef01-23456789abcd",
"name": "TD Visa *8821"
},
"category": {
"id": "d3e4f5a6-b7c8-9012-def0-123456789abc",
"name": "Office Supplies"
},
"journalEntry": null,
"receipts": [
{
"id": "f0a1b2c3-d4e5-6789-a012-bcdef0123456",
"filename": "staples-receipt-jun18.pdf",
"s3Key": "receipts/a1b2c3d4/2026/06/staples-receipt-jun18.pdf",
"uploadedAt": "2026-06-18T14:32:00Z"
}
]
}
}
}mutation {
createLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
merchantName: "Canada Post"
amount: "34.95"
transactionDate: "2026-06-20"
eventType: "expense"
categoryId: "a6b7c8d9-e0f1-2345-6789-abcdef012345"
paymentAccountId: "e4f5a6b7-c8d9-0123-ef01-23456789abcd"
) {
success
eventId
}
}{
"data": {
"createLedgerEvent": {
"success": true,
"eventId": "d9e0f1a2-b3c4-5678-d901-ef0123456789"
}
}
}mutation {
updateLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
memo: "Office supplies — printer paper and toner"
amount: "127.43"
) {
success
event {
id
memo
functionalAmount
status
}
}
}{
"data": {
"updateLedgerEvent": {
"success": true,
"event": {
"id": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"memo": "Office supplies — printer paper and toner",
"functionalAmount": "127.43",
"status": "needs_review"
}
}
}
}mutation {
confirmLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
) {
success
eventId
journalEntryId
}
}{
"data": {
"confirmLedgerEvent": {
"success": true,
"eventId": "b7c8d9e0-f1a2-3456-b789-cdef01234567",
"journalEntryId": "a2b3c4d5-e6f7-8901-ab23-cdef45678901"
}
}
}mutation {
rejectLedgerEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "c8d9e0f1-a2b3-4567-c890-def012345678"
) {
success
eventId
}
}{
"data": {
"rejectLedgerEvent": {
"success": true,
"eventId": "c8d9e0f1-a2b3-4567-c890-def012345678"
}
}
}mutation {
updateStagedEntry(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "c8d9e0f1-a2b3-4567-c890-def012345678"
categoryId: "f5a6b7c8-d9e0-1234-5678-9abcdef01234"
paymentId: "e4f5a6b7-c8d9-0123-ef01-23456789abcd"
taxAmount: "1.93"
taxAccountId: "a0b1c2d3-e4f5-6789-0abc-def123456789"
taxCode: "HST_ON"
) {
success
}
}{
"data": {
"updateStagedEntry": {
"success": true
}
}
}mutation {
attachReceiptToEvent(
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
eventId: "b7c8d9e0-f1a2-3456-b789-cdef01234567"
s3Key: "receipts/a1b2c3d4/2026/06/canadapost-shipping-label.pdf"
filename: "canadapost-shipping-label.pdf"
) {
success
receiptId
}
}{
"data": {
"attachReceiptToEvent": {
"success": true,
"receiptId": "b1c2d3e4-f5a6-7890-bcde-f01234567890"
}
}
}Accounts
List Accounts
Returns a paginated list of accounts in the chart of accounts. Supports search by name and sorting.
Parameters
organizationIdUUID!requiredOrganization ID
limitIntPage size (default 25, max 200)
offsetIntNumber of records to skip
searchStringFilter by name (case-insensitive)
sortByStringField to sort by (e.g. name, currentBalance)
sortDirStringasc or desc (default asc)
query {
listAccounts(
organizationId: "a1b2c3d4-..."
limit: 10
search: "expense"
) {
items {
id
name
accountType
classification
subType
currentBalance
active
}
totalCount
pageSize
offset
}
}{
"data": {
"listAccounts": {
"items": [
{
"id": "f7e6d5c4-...",
"name": "Office Supplies",
"accountType": "expense",
"classification": "expense",
"subType": "",
"currentBalance": 1250.00,
"active": true
}
],
"totalCount": 24,
"pageSize": 10,
"offset": 0
}
}
}Create an Account
Creates a new account in the chart of accounts.
Parameters
organizationIdUUID!requiredOrganization ID
nameString!requiredAccount name
taxCodeIdUUID!requiredCRA Tax Code code ID. Determines account type and classification. Use listTaxCodes to get available codes.
lastFourStringLast 4 digits of card/account for payment matching
parentIdUUIDParent account ID for hierarchical accounts
mutation {
createAccount(
organizationId: "a1b2c3d4-..."
name: "Office Supplies"
taxCodeId: "tax-code-uuid-for-8711"
) {
account {
id
name
accountType
classification
taxCode { code name }
}
success
error
}
}{
"data": {
"createAccount": {
"account": {
"id": "f7e6d5c4-...",
"name": "Office Supplies",
"accountType": "expense",
"classification": "expense",
"taxCode": {
"code": "8711",
"name": "Office stationery and supplies"
}
},
"success": true,
"error": null
}
}
}query {
listAccounts(
organizationId: "a1b2c3d4-..."
limit: 10
search: "expense"
) {
items {
id
name
accountType
classification
subType
currentBalance
active
}
totalCount
pageSize
offset
}
}{
"data": {
"listAccounts": {
"items": [
{
"id": "f7e6d5c4-...",
"name": "Office Supplies",
"accountType": "expense",
"classification": "expense",
"subType": "",
"currentBalance": 1250.00,
"active": true
}
],
"totalCount": 24,
"pageSize": 10,
"offset": 0
}
}
}mutation {
createAccount(
organizationId: "a1b2c3d4-..."
name: "Office Supplies"
taxCodeId: "tax-code-uuid-for-8711"
) {
account {
id
name
accountType
classification
taxCode { code name }
}
success
error
}
}{
"data": {
"createAccount": {
"account": {
"id": "f7e6d5c4-...",
"name": "Office Supplies",
"accountType": "expense",
"classification": "expense",
"taxCode": {
"code": "8711",
"name": "Office stationery and supplies"
}
},
"success": true,
"error": null
}
}
}Contacts
List Contacts
Returns a paginated list of vendors and customers.
Parameters
organizationIdUUID!requiredOrganization ID
limitIntPage size (default 25, max 200)
offsetIntNumber of records to skip
searchStringFilter by name
query {
listContacts(
organizationId: "a1b2c3d4-..."
limit: 10
search: "uber"
) {
items {
id
contactType
displayName
email
phone
balance
active
}
totalCount
}
}{
"data": {
"listContacts": {
"items": [
{
"id": "b2c3d4e5-...",
"contactType": "vendor",
"displayName": "Uber",
"email": "",
"phone": "",
"balance": 0,
"active": true
}
],
"totalCount": 1
}
}
}Create a Contact
Creates a new vendor or customer.
Parameters
organizationIdUUID!requiredOrganization ID
contactTypeString!requiredvendor or customer
displayNameString!requiredContact name
emailStringEmail address
phoneStringPhone number
mutation {
createContact(
organizationId: "a1b2c3d4-..."
contactType: "vendor"
displayName: "Staples"
email: "billing@staples.com"
) {
contact { id displayName contactType }
success
error
}
}{
"data": {
"createContact": {
"contact": {
"id": "c3d4e5f6-...",
"displayName": "Staples",
"contactType": "vendor"
},
"success": true,
"error": null
}
}
}query {
listContacts(
organizationId: "a1b2c3d4-..."
limit: 10
search: "uber"
) {
items {
id
contactType
displayName
email
phone
balance
active
}
totalCount
}
}{
"data": {
"listContacts": {
"items": [
{
"id": "b2c3d4e5-...",
"contactType": "vendor",
"displayName": "Uber",
"email": "",
"phone": "",
"balance": 0,
"active": true
}
],
"totalCount": 1
}
}
}mutation {
createContact(
organizationId: "a1b2c3d4-..."
contactType: "vendor"
displayName: "Staples"
email: "billing@staples.com"
) {
contact { id displayName contactType }
success
error
}
}{
"data": {
"createContact": {
"contact": {
"id": "c3d4e5f6-...",
"displayName": "Staples",
"contactType": "vendor"
},
"success": true,
"error": null
}
}
}Receipts
Receipts are documentary evidence attached to transactions. In Solarium V2, receipts don't create transactions directly — instead, you attach a receipt image to an existing transaction using attachReceiptToEvent (see Transactions).
Receipts can also arrive automatically via Gmail scanning or SMS/email intake addresses. These are matched to bank transactions automatically when possible, or appear as unmatched receipts for manual review.
The listReceipts query below returns all receipt records, including their OCR-extracted data, matching status, and linked transaction.
List Receipts
Returns a paginated list of receipts with OCR data, vendor, amount, and linked transaction.
Parameters
organizationIdUUID!requiredOrganization ID
limitIntPage size (default 25)
offsetIntNumber of records to skip
searchStringFilter by vendor name
statusStringFilter by status: draft, processed, reviewed, rejected
sourceStringFilter by source: web_upload, email, sms, gmail, google_drive
query {
listReceipts(
organizationId: "a1b2c3d4-..."
limit: 10
status: "draft"
) {
items {
id
vendorName
amount
receiptDate
status
source
imageUrl
event { id merchantName status }
}
totalCount
}
}{
"data": {
"listReceipts": {
"items": [
{
"id": "r1a2b3c4-...",
"vendorName": "Tim Hortons",
"amount": 8.45,
"receiptDate": "2026-01-15",
"status": "draft",
"source": "gmail",
"imageUrl": "https://s3.amazonaws.com/...",
"event": {
"id": "e5f6g7h8-...",
"merchantName": "Tim Hortons",
"status": "needs_review"
}
}
],
"totalCount": 12
}
}
}query {
listReceipts(
organizationId: "a1b2c3d4-..."
limit: 10
status: "draft"
) {
items {
id
vendorName
amount
receiptDate
status
source
imageUrl
event { id merchantName status }
}
totalCount
}
}{
"data": {
"listReceipts": {
"items": [
{
"id": "r1a2b3c4-...",
"vendorName": "Tim Hortons",
"amount": 8.45,
"receiptDate": "2026-01-15",
"status": "draft",
"source": "gmail",
"imageUrl": "https://s3.amazonaws.com/...",
"event": {
"id": "e5f6g7h8-...",
"merchantName": "Tim Hortons",
"status": "needs_review"
}
}
],
"totalCount": 12
}
}
}Reports
Dashboard Summary
High-level financial snapshot: revenue, expenses, net income, receipt counts for a date range.
Parameters
organizationIdUUID!requiredOrganization ID
startDateDateStart of range (defaults to start of current month)
endDateDateEnd of range (defaults to today)
query {
dashboardSummary(
organizationId: "a1b2c3d4-..."
startDate: "2025-01-01"
endDate: "2025-03-31"
) {
totalRevenue
totalExpenses
netIncome
receiptCount
unreviewedCount
}
}{
"data": {
"dashboardSummary": {
"totalRevenue": "84500.00",
"totalExpenses": "52300.00",
"netIncome": "32200.00",
"receiptCount": 89,
"unreviewedCount": 3
}
}
}Income Statement
Full profit & loss report broken down by account.
Parameters
organizationIdUUID!requiredOrganization ID
startDateDateStart of range
endDateDateEnd of range
query {
incomeStatement(
organizationId: "a1b2c3d4-..."
startDate: "2025-01-01"
endDate: "2025-03-31"
) {
incomeAccounts { accountId accountName total }
expenseAccounts { accountId accountName total }
totalIncome
totalExpenses
netIncome
}
}{
"data": {
"incomeStatement": {
"incomeAccounts": [
{ "accountId": "...", "accountName": "Sales", "total": "84500.00" }
],
"expenseAccounts": [
{ "accountId": "...", "accountName": "Rent", "total": "18000.00" },
{ "accountId": "...", "accountName": "Payroll", "total": "28000.00" },
{ "accountId": "...", "accountName": "Office Supplies", "total": "6300.00" }
],
"totalIncome": "84500.00",
"totalExpenses": "52300.00",
"netIncome": "32200.00"
}
}
}Account Balances
Current balance for every account in the chart of accounts.
Parameters
organizationIdUUID!requiredOrganization ID
query {
accountBalances(organizationId: "a1b2c3d4-...") {
accountId
accountName
accountType
classification
balance
}
}{
"data": {
"accountBalances": [
{
"accountId": "...",
"accountName": "Checking",
"accountType": "bank",
"classification": "asset",
"balance": "24680.50"
},
{
"accountId": "...",
"accountName": "Accounts Receivable",
"accountType": "accounts_receivable",
"classification": "asset",
"balance": "12400.00"
}
]
}
}query {
dashboardSummary(
organizationId: "a1b2c3d4-..."
startDate: "2025-01-01"
endDate: "2025-03-31"
) {
totalRevenue
totalExpenses
netIncome
receiptCount
unreviewedCount
}
}{
"data": {
"dashboardSummary": {
"totalRevenue": "84500.00",
"totalExpenses": "52300.00",
"netIncome": "32200.00",
"receiptCount": 89,
"unreviewedCount": 3
}
}
}query {
incomeStatement(
organizationId: "a1b2c3d4-..."
startDate: "2025-01-01"
endDate: "2025-03-31"
) {
incomeAccounts { accountId accountName total }
expenseAccounts { accountId accountName total }
totalIncome
totalExpenses
netIncome
}
}{
"data": {
"incomeStatement": {
"incomeAccounts": [
{ "accountId": "...", "accountName": "Sales", "total": "84500.00" }
],
"expenseAccounts": [
{ "accountId": "...", "accountName": "Rent", "total": "18000.00" },
{ "accountId": "...", "accountName": "Payroll", "total": "28000.00" },
{ "accountId": "...", "accountName": "Office Supplies", "total": "6300.00" }
],
"totalIncome": "84500.00",
"totalExpenses": "52300.00",
"netIncome": "32200.00"
}
}
}query {
accountBalances(organizationId: "a1b2c3d4-...") {
accountId
accountName
accountType
classification
balance
}
}{
"data": {
"accountBalances": [
{
"accountId": "...",
"accountName": "Checking",
"accountType": "bank",
"classification": "asset",
"balance": "24680.50"
},
{
"accountId": "...",
"accountName": "Accounts Receivable",
"accountType": "accounts_receivable",
"classification": "asset",
"balance": "12400.00"
}
]
}
}Bank Import
Import bank statement rows as transactions. Each row becomes a LedgerEvent with status needs_review. Use checkBankStatementDups first to detect duplicates.
Check Bank Statement Duplicates
Check rows against existing transactions before importing. Returns per-row duplicate flags. Full match = same date + amount + vendor. Possible match = same date + amount, different vendor.
Parameters
organizationIdUUID!requiredOrganization ID
paymentAccountIdUUID!requiredBank or credit card account to check against
rows[BankStatementRowInput!]!requiredArray of statement rows to check. Each row: transactionDate (String!), description (String!), amount (String!), eventType (String!), memo (String), reference (String)
mutation {
checkBankStatementDups(
organizationId: "a1b2c3d4-..."
paymentAccountId: "chequing-uuid"
rows: [
{
transactionDate: "2025-03-01"
description: "SHOPIFY *MAPLE ROASTERS"
amount: "149.50"
eventType: "expense"
},
{
transactionDate: "2025-03-02"
description: "TIM HORTONS #4821"
amount: "11.30"
eventType: "expense"
memo: "Team coffee run"
}
]
) {
results {
rowIndex
isDuplicate
isPossibleDuplicate
matchingEventId
}
success
}
}{
"data": {
"checkBankStatementDups": {
"results": [
{
"rowIndex": 0,
"isDuplicate": false,
"isPossibleDuplicate": false,
"matchingEventId": null
},
{
"rowIndex": 1,
"isDuplicate": true,
"isPossibleDuplicate": false,
"matchingEventId": "d8e9f0a1-..."
}
],
"success": true
}
}
}Import Bank Statement
Imports bank statement rows as LedgerEvents attached to a payment account. Each row is created with status needs_review. Rows that exactly match existing transactions (same date, amount, and vendor) are skipped automatically.
Parameters
organizationIdUUID!requiredOrganization ID
paymentAccountIdUUID!requiredBank or credit card account to import into
currencyStringISO 4217 currency code (default "CAD")
rows[BankStatementRowInput!]!requiredArray of statement rows. Each row: transactionDate (String!), description (String!), amount (String!), eventType (String!), memo (String), reference (String)
mutation {
importBankStatement(
organizationId: "a1b2c3d4-..."
paymentAccountId: "chequing-uuid"
currency: "CAD"
rows: [
{
transactionDate: "2025-03-01"
description: "SHOPIFY *MAPLE ROASTERS"
amount: "149.50"
eventType: "expense"
reference: "TXN-20250301-001"
},
{
transactionDate: "2025-03-01"
description: "E-TRANSFER FROM CLAIRE DUBOIS"
amount: "2500.00"
eventType: "income"
memo: "Invoice #1042 payment"
},
{
transactionDate: "2025-03-03"
description: "CANADA POST CORP"
amount: "18.75"
eventType: "expense"
memo: "Shipping label — order #887"
reference: "CP-88712345"
}
]
) {
importedCount
skippedCount
success
error
}
}{
"data": {
"importBankStatement": {
"importedCount": 3,
"skippedCount": 0,
"success": true,
"error": null
}
}
}mutation {
checkBankStatementDups(
organizationId: "a1b2c3d4-..."
paymentAccountId: "chequing-uuid"
rows: [
{
transactionDate: "2025-03-01"
description: "SHOPIFY *MAPLE ROASTERS"
amount: "149.50"
eventType: "expense"
},
{
transactionDate: "2025-03-02"
description: "TIM HORTONS #4821"
amount: "11.30"
eventType: "expense"
memo: "Team coffee run"
}
]
) {
results {
rowIndex
isDuplicate
isPossibleDuplicate
matchingEventId
}
success
}
}{
"data": {
"checkBankStatementDups": {
"results": [
{
"rowIndex": 0,
"isDuplicate": false,
"isPossibleDuplicate": false,
"matchingEventId": null
},
{
"rowIndex": 1,
"isDuplicate": true,
"isPossibleDuplicate": false,
"matchingEventId": "d8e9f0a1-..."
}
],
"success": true
}
}
}mutation {
importBankStatement(
organizationId: "a1b2c3d4-..."
paymentAccountId: "chequing-uuid"
currency: "CAD"
rows: [
{
transactionDate: "2025-03-01"
description: "SHOPIFY *MAPLE ROASTERS"
amount: "149.50"
eventType: "expense"
reference: "TXN-20250301-001"
},
{
transactionDate: "2025-03-01"
description: "E-TRANSFER FROM CLAIRE DUBOIS"
amount: "2500.00"
eventType: "income"
memo: "Invoice #1042 payment"
},
{
transactionDate: "2025-03-03"
description: "CANADA POST CORP"
amount: "18.75"
eventType: "expense"
memo: "Shipping label — order #887"
reference: "CP-88712345"
}
]
) {
importedCount
skippedCount
success
error
}
}{
"data": {
"importBankStatement": {
"importedCount": 3,
"skippedCount": 0,
"success": true,
"error": null
}
}
}Category Rules
Auto-categorization rules map vendor name patterns to accounts. When a transaction's merchant name matches a rule's pattern, Solarium automatically fills in the expense category during import and review. Rules are deterministic and auditable — they complement the AI agent's suggestions with a predictable, user-controlled layer.
List Category Rules
List auto-categorization rules. Rules map vendor name patterns to accounts. When a transaction's merchant matches a pattern, Solarium auto-fills the category.
Parameters
organizationIdUUID!requiredOrganization ID
limitIntPage size (default 25, max 200)
offsetIntNumber of records to skip
searchStringFilter by vendor pattern (case-insensitive)
sortByStringField to sort by (e.g. vendorPattern, matchCount)
sortDirStringasc or desc (default asc)
query {
listCategoryRules(
organizationId: "a1b2c3d4-..."
limit: 10
search: "tim"
) {
items {
id
vendorPattern
account { id name }
matchCount
}
totalCount
pageSize
offset
}
}{
"data": {
"listCategoryRules": {
"items": [
{
"id": "r1a2b3c4-...",
"vendorPattern": "%TIM HORTONS%",
"account": {
"id": "f7e6d5c4-...",
"name": "Meals & Entertainment"
},
"matchCount": 34
},
{
"id": "r5d6e7f8-...",
"vendorPattern": "%TIMMIES%",
"account": {
"id": "f7e6d5c4-...",
"name": "Meals & Entertainment"
},
"matchCount": 3
}
],
"totalCount": 2,
"pageSize": 10,
"offset": 0
}
}
}Create a Category Rule
Create an auto-categorization rule. The vendorPattern is matched case-insensitively against merchant names. Use SQL ILIKE patterns (% for wildcard).
Parameters
organizationIdUUID!requiredOrganization ID
vendorPatternString!requiredILIKE pattern to match vendor names (e.g. "%SHOPIFY%", "%CANADA POST%")
categoryIdUUID!requiredAccount ID to auto-assign when pattern matches
mutation {
createCategoryRule(
organizationId: "a1b2c3d4-..."
vendorPattern: "%PETRO-CANADA%"
categoryId: "vehicle-expense-uuid"
) {
categoryRule {
id
vendorPattern
account { id name }
}
success
error
}
}{
"data": {
"createCategoryRule": {
"categoryRule": {
"id": "r9a8b7c6-...",
"vendorPattern": "%PETRO-CANADA%",
"account": {
"id": "vehicle-expense-uuid",
"name": "Vehicle Expenses"
}
},
"success": true,
"error": null
}
}
}query {
listCategoryRules(
organizationId: "a1b2c3d4-..."
limit: 10
search: "tim"
) {
items {
id
vendorPattern
account { id name }
matchCount
}
totalCount
pageSize
offset
}
}{
"data": {
"listCategoryRules": {
"items": [
{
"id": "r1a2b3c4-...",
"vendorPattern": "%TIM HORTONS%",
"account": {
"id": "f7e6d5c4-...",
"name": "Meals & Entertainment"
},
"matchCount": 34
},
{
"id": "r5d6e7f8-...",
"vendorPattern": "%TIMMIES%",
"account": {
"id": "f7e6d5c4-...",
"name": "Meals & Entertainment"
},
"matchCount": 3
}
],
"totalCount": 2,
"pageSize": 10,
"offset": 0
}
}
}mutation {
createCategoryRule(
organizationId: "a1b2c3d4-..."
vendorPattern: "%PETRO-CANADA%"
categoryId: "vehicle-expense-uuid"
) {
categoryRule {
id
vendorPattern
account { id name }
}
success
error
}
}{
"data": {
"createCategoryRule": {
"categoryRule": {
"id": "r9a8b7c6-...",
"vendorPattern": "%PETRO-CANADA%",
"account": {
"id": "vehicle-expense-uuid",
"name": "Vehicle Expenses"
}
},
"success": true,
"error": null
}
}
}Batch Operations
All batch mutations accept up to 500 items per call. Failures are per-item — one bad record does not roll back the others. Each item runs in its own database savepoint.
For bulk transaction import, use importBankStatement (see Bank Import) which accepts an array of rows and creates LedgerEvents.
Batch Create Accounts
Create up to 500 accounts in a single call.
Parameters
organizationIdUUID!requiredOrganization ID
items[AccountInput!]!requiredArray of accounts. Each: name (String!), accountType (String!), classification (String!), subType (String), parentId (UUID)
mutation {
batchCreateAccounts(
organizationId: "a1b2c3d4-..."
items: [
{ name: "Office Supplies", accountType: "expense", classification: "expense" },
{ name: "Travel", accountType: "expense", classification: "expense" },
{ name: "Software", accountType: "expense", classification: "expense" }
]
) {
created
errors { index error }
}
}{
"data": {
"batchCreateAccounts": {
"created": 3,
"errors": []
}
}
}Batch Create Contacts
Create up to 500 contacts in a single call.
Parameters
organizationIdUUID!requiredOrganization ID
items[ContactInput!]!requiredArray of contacts. Each: contactType (String!), displayName (String!), email (String), phone (String)
mutation {
batchCreateContacts(
organizationId: "a1b2c3d4-..."
items: [
{ contactType: "vendor", displayName: "Staples" },
{ contactType: "vendor", displayName: "Amazon" }
]
) {
created
errors { index error }
}
}{
"data": {
"batchCreateContacts": {
"created": 2,
"errors": []
}
}
}mutation {
batchCreateAccounts(
organizationId: "a1b2c3d4-..."
items: [
{ name: "Office Supplies", accountType: "expense", classification: "expense" },
{ name: "Travel", accountType: "expense", classification: "expense" },
{ name: "Software", accountType: "expense", classification: "expense" }
]
) {
created
errors { index error }
}
}{
"data": {
"batchCreateAccounts": {
"created": 3,
"errors": []
}
}
}mutation {
batchCreateContacts(
organizationId: "a1b2c3d4-..."
items: [
{ contactType: "vendor", displayName: "Staples" },
{ contactType: "vendor", displayName: "Amazon" }
]
) {
created
errors { index error }
}
}{
"data": {
"batchCreateContacts": {
"created": 2,
"errors": []
}
}
}Intake Addresses
List Intake Addresses
Returns all registered email addresses and phone numbers that route receipts to the organization.
Parameters
organizationIdUUID!requiredOrganization ID
query {
listIntakeAddresses(organizationId: "a1b2c3d4-...") {
id
addressType
address
label
active
created
}
}{
"data": {
"listIntakeAddresses": [
{
"id": "e1f2a3b4-...",
"addressType": "email",
"address": "accounting@company.com",
"label": "Main Office",
"active": true,
"created": "2026-05-12T10:30:00Z"
},
{
"id": "c5d6e7f8-...",
"addressType": "sms",
"address": "14165551234",
"label": "Owner",
"active": true,
"created": "2026-05-12T10:00:00Z"
}
]
}
}Create Intake Address
Register a new email address or phone number for receipt intake. Each address is globally unique — it can only belong to one organization.
Parameters
organizationIdUUID!requiredOrganization ID
addressTypeString!required"email" or "sms"
addressString!requiredEmail address or phone number (digits only for SMS)
labelStringOptional display label (e.g. 'Main Office')
mutation {
createIntakeAddress(
organizationId: "a1b2c3d4-..."
addressType: "email"
address: "receipts@company.com"
label: "Shared Inbox"
) {
intakeAddress {
id
addressType
address
label
}
success
error
}
}{
"data": {
"createIntakeAddress": {
"intakeAddress": {
"id": "f9a8b7c6-...",
"addressType": "email",
"address": "receipts@company.com",
"label": "Shared Inbox"
},
"success": true,
"error": null
}
}
}Delete Intake Address
Remove a registered intake address. Receipts from this address will no longer route to the organization.
Parameters
organizationIdUUID!requiredOrganization ID
intakeAddressIdUUID!requiredIntake address ID to remove
mutation {
deleteIntakeAddress(
organizationId: "a1b2c3d4-..."
intakeAddressId: "f9a8b7c6-..."
) {
success
error
}
}{
"data": {
"deleteIntakeAddress": {
"success": true,
"error": null
}
}
}query {
listIntakeAddresses(organizationId: "a1b2c3d4-...") {
id
addressType
address
label
active
created
}
}{
"data": {
"listIntakeAddresses": [
{
"id": "e1f2a3b4-...",
"addressType": "email",
"address": "accounting@company.com",
"label": "Main Office",
"active": true,
"created": "2026-05-12T10:30:00Z"
},
{
"id": "c5d6e7f8-...",
"addressType": "sms",
"address": "14165551234",
"label": "Owner",
"active": true,
"created": "2026-05-12T10:00:00Z"
}
]
}
}mutation {
createIntakeAddress(
organizationId: "a1b2c3d4-..."
addressType: "email"
address: "receipts@company.com"
label: "Shared Inbox"
) {
intakeAddress {
id
addressType
address
label
}
success
error
}
}{
"data": {
"createIntakeAddress": {
"intakeAddress": {
"id": "f9a8b7c6-...",
"addressType": "email",
"address": "receipts@company.com",
"label": "Shared Inbox"
},
"success": true,
"error": null
}
}
}mutation {
deleteIntakeAddress(
organizationId: "a1b2c3d4-..."
intakeAddressId: "f9a8b7c6-..."
) {
success
error
}
}{
"data": {
"deleteIntakeAddress": {
"success": true,
"error": null
}
}
}Stripe Import
A management command that imports Stripe balance_history.csv exports into Solarium as transactions (LedgerEvents). Works with Stripe Connect platforms and direct Stripe accounts.
Uses the gross revenue method — charges are booked as income (Sales Revenue), connected-account payouts as expense (Cost of Goods Sold).
| CSV Row Type | Event Type | Category |
|---|---|---|
| charge | income | Sales Revenue |
| transfer (merchant payout) | expense | Cost of Goods Sold |
| stripe_fee | expense | Bank Fees & Charges |
| refund | refund | Sales Revenue |
| payout (to bank) | transfer | Stripe → Chequing Account |
| application_fee | skipped | Implicit in gross |
- Deduplicates by Stripe source ID stored in
backfill_id— safe to re-run - Each run gets a batch ID for clean reversal via
delete_stripe_imports --dry-runpreviews without writing--org-nameis required (no default)
Import Command
Run on the server to import a Stripe balance history CSV.
# Dry run — preview what would be created
python manage.py import_stripe_balance ~/balance_history.csv \
--org-name "My Company Inc." --dry-run
# Real import
python manage.py import_stripe_balance ~/balance_history.csv \
--org-name "My Company Inc."
# Verify idempotency (should create 0, skip all)
python manage.py import_stripe_balance ~/balance_history.csv \
--org-name "My Company Inc."# Dry run output:
2026-06-21 income $ 23.55 Stripe Revenue
2026-06-21 expense $ 13.21 Stripe Merchant Payout tr_1Tkqw...
2026-06-17 expense $ 3.20 Stripe Fee
2026-06-04 transfer $ 1300.00 Stripe Payout To Bank
2026-06-04 refund $ 4.99 Stripe Refund
Batch: si:20260621235202
Created: 61, Skipped (app_fee): 9, Skipped (dupe): 0
(Dry Run — Nothing Written)
# Real run output:
Batch: si:20260621235202
Created: 61, Skipped (app_fee): 9, Skipped (dupe): 0
# Re-run output (idempotent):
Created: 0, Skipped (app_fee): 9, Skipped (dupe): 61Reverse Import
Delete all events from a specific import batch, or all Stripe imports for an org.
# Delete a specific batch
python manage.py delete_stripe_imports \
--org-name "My Company Inc." \
--batch-id si:20260621235202
# Delete all Stripe imports for an org
python manage.py delete_stripe_imports \
--org-name "My Company Inc."
# Dry run first
python manage.py delete_stripe_imports \
--org-name "My Company Inc." --dry-run# Dry run:
Found 61 Stripe Import Events To Delete.
Would Delete: 2026-06-21 $23.55 Stripe Revenue (si:20260621235202:ch_3Tknn1...)
Would Delete: 2026-06-21 $13.21 Stripe Merchant Payout tr_1Tkqw... (si:20260621235202:tr_1Tkqw...)
... And 59 More
# Real delete:
Deleted 61 Stripe Import Events.# Dry run — preview what would be created
python manage.py import_stripe_balance ~/balance_history.csv \
--org-name "My Company Inc." --dry-run
# Real import
python manage.py import_stripe_balance ~/balance_history.csv \
--org-name "My Company Inc."
# Verify idempotency (should create 0, skip all)
python manage.py import_stripe_balance ~/balance_history.csv \
--org-name "My Company Inc."# Dry run output:
2026-06-21 income $ 23.55 Stripe Revenue
2026-06-21 expense $ 13.21 Stripe Merchant Payout tr_1Tkqw...
2026-06-17 expense $ 3.20 Stripe Fee
2026-06-04 transfer $ 1300.00 Stripe Payout To Bank
2026-06-04 refund $ 4.99 Stripe Refund
Batch: si:20260621235202
Created: 61, Skipped (app_fee): 9, Skipped (dupe): 0
(Dry Run — Nothing Written)
# Real run output:
Batch: si:20260621235202
Created: 61, Skipped (app_fee): 9, Skipped (dupe): 0
# Re-run output (idempotent):
Created: 0, Skipped (app_fee): 9, Skipped (dupe): 61# Delete a specific batch
python manage.py delete_stripe_imports \
--org-name "My Company Inc." \
--batch-id si:20260621235202
# Delete all Stripe imports for an org
python manage.py delete_stripe_imports \
--org-name "My Company Inc."
# Dry run first
python manage.py delete_stripe_imports \
--org-name "My Company Inc." --dry-run# Dry run:
Found 61 Stripe Import Events To Delete.
Would Delete: 2026-06-21 $23.55 Stripe Revenue (si:20260621235202:ch_3Tknn1...)
Would Delete: 2026-06-21 $13.21 Stripe Merchant Payout tr_1Tkqw... (si:20260621235202:tr_1Tkqw...)
... And 59 More
# Real delete:
Deleted 61 Stripe Import Events.Venn / Bank CSV Import
Import bank transaction CSVs (Venn, TD, or any bank) into Solarium as transactions via the importBankStatement API or the CSV import page in the admin UI.
The CSV import flow:
1. Upload CSV on the Import page
2. Map columns (date, description, amount)
3. Select payment account
4. Preview rows with duplicate detection
5. Import — each row becomes a LedgerEvent with needs_review status
| Transaction Type | Event Type |
|---|---|
| Card payments (expenses) | expense |
| Loads / funding | transfer |
| Interest / cashback | income |
Duplicate detection runs automatically: exact matches (same date + amount + vendor) are flagged as duplicates, partial matches (same date + amount, different vendor) as possible duplicates.
CSV Import via API
Use the importBankStatement mutation to import rows programmatically.
mutation {
importBankStatement(
organizationId: "a1b2c3d4-..."
paymentAccountId: "bank-account-uuid"
currency: "CAD"
rows: [
{
transactionDate: "2026-06-19"
description: "MVC INC."
amount: "-23.55"
eventType: "expense"
},
{
transactionDate: "2026-06-13"
description: "Account Funding"
amount: "500.00"
eventType: "income"
}
]
) {
importedCount
skippedCount
success
error
}
}{
"data": {
"importBankStatement": {
"importedCount": 2,
"skippedCount": 0,
"success": true,
"error": null
}
}
}Check Duplicates Before Import
Run checkBankStatementDups first to detect duplicates. Returns per-row flags so you can skip or override.
mutation {
checkBankStatementDups(
organizationId: "a1b2c3d4-..."
paymentAccountId: "bank-account-uuid"
rows: [
{
transactionDate: "2026-06-19"
description: "MVC INC."
amount: "23.55"
eventType: "expense"
}
]
) {
results {
rowIndex
isDuplicate
isPossibleDuplicate
matchingEventId
}
success
}
}{
"data": {
"checkBankStatementDups": {
"results": [
{
"rowIndex": 0,
"isDuplicate": true,
"isPossibleDuplicate": false,
"matchingEventId": "e5f6g7h8-..."
}
],
"success": true
}
}
}mutation {
importBankStatement(
organizationId: "a1b2c3d4-..."
paymentAccountId: "bank-account-uuid"
currency: "CAD"
rows: [
{
transactionDate: "2026-06-19"
description: "MVC INC."
amount: "-23.55"
eventType: "expense"
},
{
transactionDate: "2026-06-13"
description: "Account Funding"
amount: "500.00"
eventType: "income"
}
]
) {
importedCount
skippedCount
success
error
}
}{
"data": {
"importBankStatement": {
"importedCount": 2,
"skippedCount": 0,
"success": true,
"error": null
}
}
}mutation {
checkBankStatementDups(
organizationId: "a1b2c3d4-..."
paymentAccountId: "bank-account-uuid"
rows: [
{
transactionDate: "2026-06-19"
description: "MVC INC."
amount: "23.55"
eventType: "expense"
}
]
) {
results {
rowIndex
isDuplicate
isPossibleDuplicate
matchingEventId
}
success
}
}{
"data": {
"checkBankStatementDups": {
"results": [
{
"rowIndex": 0,
"isDuplicate": true,
"isPossibleDuplicate": false,
"matchingEventId": "e5f6g7h8-..."
}
],
"success": true
}
}
}