GraphQL API Overview
Crane Ledger provides a powerful GraphQL API that enables flexible, efficient querying of your accounting data. Unlike traditional REST APIs that require multiple round-trips for complex data relationships, GraphQL allows you to fetch exactly the data you need in a single request.
Why GraphQL?
Traditional REST API Limitations
Multiple Round Trips:
// REST: Need multiple requests for related data
const [org, accounts, recentTxns] = await Promise.all([
fetch('/organizations/org_123'),
fetch('/organizations/org_123/accounts'),
fetch('/organizations/org_123/transactions?limit=10')
]);
const orgData = await org.json();
const accountsData = await accounts.json();
const txnsData = await txns.json();
Over-fetching or Under-fetching:
// REST: Either get too much data...
const account = await fetch('/accounts/act_456'); // Returns ALL fields
// ...or make multiple requests for related data
const account = await fetch('/accounts/act_456');
const balance = await fetch('/accounts/act_456/balance');
const transactions = await fetch('/accounts/act_456/transactions');
GraphQL Solution
Single Request, Exact Data:
query GetAccountOverview($accountId: ID!) {
account(id: $accountId) {
id
code
name
balance {
amount
formatted
}
transactions(limit: 5) {
description
amount
date
}
}
}
Architecture
GraphQL Layer Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GraphQL │ │ REST API │ │ Database │
│ Schema │ │ Endpoints │ │ (PostgreSQL) │
│ │ │ │ │ │
│ • Queries │◄──►│ • GET /orgs │◄──►│ • organizations │
│ • Mutations │ │ • POST /txns │ │ • accounts │
│ • Resolvers │ │ • PUT /contacts │ │ • transactions │
│ • Types │ │ • DELETE /bills │ │ • invoices │
└─────────────────┘ └─────────────────┘ └─────────────────┘
How It Works
- Client sends GraphQL query to
/graphqlendpoint - GraphQL server parses the query and validates against schema
- Resolvers execute by calling REST API endpoints
- Data aggregated and returned in single response
- Credits consumed based on underlying REST operations
Benefits
✅ Single Endpoint: /graphql handles all operations ✅ Type Safety: Strongly typed schema prevents errors ✅ Introspection: API self-documents with __schema queries ✅ Versionless: Add fields without breaking changes ✅ Efficient: Fetch related data without over/under-fetching
Authentication
API Key Authentication
All GraphQL requests require authentication via API key:
# Include in request headers
Authorization: Bearer cl_live_your_api_key_here
# or
Authorization: Bearer cl_test_your_api_key_here
Organization Scoping
API keys are automatically scoped to your organization:
# This query automatically uses your API key's organization
query GetMyOrganization {
organization {
id
name
accounts {
code
name
}
}
}
Authentication Context
The GraphQL context includes:
- API Key: Full key details and permissions
- Organization ID: Automatically determined from key
- User Permissions: Access control for operations
Schema Structure
Root Types
type Query {
# Organization queries
organization: OrganizationNode
organizationById(id: ID!): OrganizationNode
# Entity queries
account(id: ID!): AccountNode
transaction(id: ID!): TransactionNode
contact(id: ID!): ContactNode
invoice(id: ID!): InvoiceNode
bill(id: ID!): BillNode
# System queries
currencies: [CurrencyNode!]!
}
type Mutation {
# Account operations
createAccount(code: String!, name: String!, accountType: AccountType!, description: String): AccountNode
updateAccount(id: ID!, code: String, name: String, description: String): AccountNode
deleteAccount(id: ID!): Boolean
# Transaction operations
createTransaction(description: String!, date: DateTime!, entries: [LedgerEntryInput!]!): TransactionNode
# Contact operations
createContact(name: String!, contactType: ContactType!, email: String, phone: String, taxId: String): ContactNode
updateContact(id: ID!, name: String, email: String, phone: String, taxId: String): ContactNode
deleteContact(id: ID!): Boolean
# Document operations
createInvoice(contactId: ID!, invoiceDate: DateTime!, dueDate: DateTime!, items: [InvoiceItemInput!]!, notes: String): InvoiceNode
createBill(contactId: ID!, billDate: DateTime!, dueDate: DateTime!, items: [BillItemInput!]!, notes: String): BillNode
recordInvoicePayment(invoiceId: ID!, amount: Decimal!, paymentDate: DateTime!, paymentMethod: String!, reference: String, notes: String): PaymentNode
recordBillPayment(billId: ID!, amount: Decimal!, paymentDate: DateTime!, paymentMethod: String!, reference: String, notes: String): PaymentNode
# Billing operations
purchaseCredits(creditAmount: Int!, successUrl: String, cancelUrl: String): CreditPurchaseNode
}
Core Entity Types
type OrganizationNode {
id: ID!
name: String!
baseCurrency: CurrencyNode!
isRoot: Boolean!
parentOrganizationId: ID
creditsRemaining: Int!
createdAt: DateTime!
updatedAt: DateTime!
metadata: Json!
# Complex resolvers
accounts: [AccountNode!]!
recentTransactions(limit: Int): [TransactionNode!]!
contacts: [ContactNode!]!
invoices: [InvoiceNode!]!
bills: [BillNode!]!
trialBalance: TrialBalanceReport!
balanceSheet: BalanceSheetReport!
incomeStatement: IncomeStatementReport!
creditBalance: CreditBalanceNode!
creditPackages: [CreditPackageNode!]!
}
type AccountNode {
id: ID!
organizationId: ID!
code: String!
name: String!
accountType: AccountType!
parentAccountId: ID
status: AccountStatus!
description: String
isSystem: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
metadata: Json!
# Complex resolvers
balance: MoneyNode!
transactions(limit: Int): [TransactionNode!]!
}
type TransactionNode {
id: ID!
organizationId: ID!
description: String!
date: DateTime!
status: TransactionStatus!
amount: Decimal!
currency: CurrencyNode!
entries: [LedgerEntryNode!]!
createdAt: DateTime!
updatedAt: DateTime!
metadata: Json!
}
Getting Started
1. Choose Your Client
JavaScript/TypeScript:
npm install graphql-request
Python:
pip install gql
cURL:
# Direct HTTP requests
curl -X POST https://api.craneledger.ai/graphql \
-H "Authorization: Bearer cl_live_your_key" \
-H "Content-Type: application/json" \
-d '{"query": "query { organization { name } }"}'
2. Explore with Playground
Visit the GraphQL playground for interactive exploration:
https://api.craneledger.ai/graphql/playground
3. Your First Query
query GetStarted {
organization {
id
name
baseCurrency {
code
name
symbol
}
creditsRemaining
}
}
4. Create Your First Account
mutation CreateCheckingAccount {
createAccount(
code: "1001"
name: "Operating Checking"
accountType: ASSET
description: "Primary business checking account"
) {
id
code
name
accountType
}
}
Credit Consumption
Understanding Costs
GraphQL operations consume credits based on underlying REST calls:
| Operation | Credits | Description |
|---|---|---|
| Simple Queries | 0 | Basic field resolution (free) |
| Complex Queries | 0 | Resolvers calling REST endpoints (free) |
| Mutations | 1-10 | Based on underlying operation complexity |
| Report Generation | 5 | Financial statement creation |
Cost Examples
# Free: Basic organization info
query BasicOrg { organization { name } }
# Cost: 0 credits (free)
# Free: With related data
query OrgWithAccounts {
organization {
name
accounts { code name }
}
}
# Cost: 0 credits (free)
# Higher cost: Financial reports
query GetReports {
organization {
trialBalance { totalDebits }
balanceSheet { totalAssets }
}
}
# Cost: 10 credits (5 per report)
# Mutation cost: Create account
mutation CreateAccount { ... }
# Cost: 1 credit
Error Handling
Common Error Types
{
"errors": [
{
"message": "Authentication required",
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
{
"errors": [
{
"message": "Field 'invalidField' is not defined on type 'OrganizationNode'",
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED"
}
}
]
}
Business Logic Errors
{
"errors": [
{
"message": "Transaction does not balance: debits (100.00) != credits (50.00)",
"extensions": {
"code": "BUSINESS_RULE_VIOLATION"
}
}
]
}
Best Practices
Query Design
- Request Only What You Need:
# ✅ Good: Specific fields
query { organization { name creditsRemaining } }
# ❌ Bad: Everything
query { organization }
- Use Fragments for Reuse:
fragment AccountFields on AccountNode {
id
code
name
balance { formatted }
}
query GetAccounts {
organization {
accounts {
...AccountFields
}
}
}
- Pagination for Large Datasets:
query GetRecentTransactions {
organization {
recentTransactions(limit: 50) {
id
description
amount
}
}
}
Performance Optimization
- Batch Related Operations:
# ✅ Single query for related data
query GetInvoiceDetails($invoiceId: ID!) {
invoice(id: $invoiceId) {
id
total
items { description quantity unitPrice }
payments { amount paymentDate }
}
}
# ❌ Multiple separate queries
- Cache Static Data:
// Cache currencies, account types, etc.
const currencies = await client.query({ query: GET_CURRENCIES });
const accountTypes = await client.query({ query: GET_ACCOUNT_TYPES });
- Monitor Credit Usage:
// Track costs in development
const response = await client.query({ query: MY_QUERY });
console.log('Credits used:', response.extensions?.creditsUsed);
Integration Examples
React Application
import { useQuery, useMutation, gql } from '@apollo/client';
const GET_ORGANIZATION = gql`
query GetOrganization {
organization {
id
name
accounts {
id
code
name
balance {
formatted
}
}
}
}
`;
function OrganizationDashboard() {
const { loading, error, data } = useQuery(GET_ORGANIZATION);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{data.organization.name}</h1>
{data.organization.accounts.map(account => (
<div key={account.id}>
{account.code} - {account.name}: {account.balance.formatted}
</div>
))}
</div>
);
}
Python Integration
import gql
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(
url="https://api.craneledger.ai/graphql",
headers={"Authorization": "Bearer cl_live_your_key"}
)
client = gql.Client(transport=transport)
query = gql.gql("""
query GetTrialBalance {
organization {
trialBalance {
totalDebits
totalCredits
accounts {
accountCode
accountName
netBalance
}
}
}
}
""")
result = await client.execute_async(query)
Comparison with REST API
| Feature | GraphQL | REST |
|---|---|---|
| Data Fetching | Single request, exact data | Multiple requests, fixed endpoints |
| Over-fetching | ❌ No over-fetching | ✅ Can over-fetch |
| Under-fetching | ❌ No under-fetching | ✅ Multiple requests needed |
| API Evolution | ✅ Add fields without breaking | ⚠️ Versioning required |
| Documentation | ✅ Self-documenting schema | ⚠️ Manual documentation |
| Type Safety | ✅ Strongly typed | ⚠️ Runtime validation |
| Learning Curve | ⚠️ New concepts | ✅ Familiar HTTP |
| Caching | ⚠️ More complex | ✅ Standard HTTP caching |
Playground & Development Tools
GraphQL Playground
Access the interactive playground at:
https://api.craneledger.ai/graphql/playground
Features:
- Auto-completion of queries and mutations
- Schema exploration with type documentation
- Query history and saved queries
- Real-time validation with error highlighting
- Response formatting and export options
Development Workflow
- Explore Schema:
query IntrospectSchema {
__schema {
types {
name
description
}
}
}
- Test Queries:
# Use playground to test before implementing
query TestQuery {
organization {
name
accounts {
code
name
}
}
}
- Monitor Performance:
# Check query complexity
query AnalyzeQuery {
organization {
accounts {
transactions(limit: 100) {
entries {
account {
name
}
}
}
}
}
}
Migration from REST
REST to GraphQL Migration
Before (REST):
// Multiple round trips
async function getInvoiceDetails(invoiceId) {
const invoice = await api.get(`/invoices/${invoiceId}`);
const items = await api.get(`/invoices/${invoiceId}/items`);
const payments = await api.get(`/invoices/${invoiceId}/payments`);
const contact = await api.get(`/contacts/${invoice.contactId}`);
return { invoice, items, payments, contact };
}
After (GraphQL):
const GET_INVOICE_DETAILS = gql`
query GetInvoiceDetails($invoiceId: ID!) {
invoice(id: $invoiceId) {
id
total
status
contact {
name
email
}
items {
description
quantity
unitPrice
lineTotal
}
payments {
amount
paymentDate
paymentMethod
}
}
}
`;
// Single request
const result = await client.query({
query: GET_INVOICE_DETAILS,
variables: { invoiceId }
});
Gradual Migration Strategy
- Start with Read Operations: Replace GET requests with GraphQL queries
- Add Complex Queries: Combine multiple REST calls into single GraphQL queries
- Migrate Mutations: Replace POST/PUT/DELETE with GraphQL mutations
- Optimize Performance: Use GraphQL's efficiency for better user experience
Crane Ledger's GraphQL API provides the flexibility and efficiency modern applications demand. Whether you're building a sophisticated financial dashboard or integrating accounting into your existing workflow, GraphQL enables you to fetch exactly the data you need, exactly when you need it.
Ready to explore GraphQL? Visit the GraphQL Playground and start querying your accounting data with precision and efficiency.
Need help?
Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.
- ✨ For LLMs/AI assistants: Read our structured API reference