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

  1. Client sends GraphQL query to /graphql endpoint
  2. GraphQL server parses the query and validates against schema
  3. Resolvers execute by calling REST API endpoints
  4. Data aggregated and returned in single response
  5. 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:

OperationCreditsDescription
Simple Queries0Basic field resolution (free)
Complex Queries0Resolvers calling REST endpoints (free)
Mutations1-10Based on underlying operation complexity
Report Generation5Financial 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

  1. Request Only What You Need:
# ✅ Good: Specific fields
query { organization { name creditsRemaining } }

# ❌ Bad: Everything
query { organization }
  1. Use Fragments for Reuse:
fragment AccountFields on AccountNode {
  id
  code
  name
  balance { formatted }
}

query GetAccounts {
  organization {
    accounts {
      ...AccountFields
    }
  }
}
  1. Pagination for Large Datasets:
query GetRecentTransactions {
  organization {
    recentTransactions(limit: 50) {
      id
      description
      amount
    }
  }
}

Performance Optimization

  1. 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
  1. 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 });
  1. 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

FeatureGraphQLREST
Data FetchingSingle request, exact dataMultiple 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

  1. Explore Schema:
query IntrospectSchema {
  __schema {
    types {
      name
      description
    }
  }
}
  1. Test Queries:
# Use playground to test before implementing
query TestQuery {
  organization {
    name
    accounts {
      code
      name
    }
  }
}
  1. 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

  1. Start with Read Operations: Replace GET requests with GraphQL queries
  2. Add Complex Queries: Combine multiple REST calls into single GraphQL queries
  3. Migrate Mutations: Replace POST/PUT/DELETE with GraphQL mutations
  4. 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.