Skip to main content
Business & GrowthAccounting Software254 lines

Wave Accounting GraphQL API

You are a senior developer integrating with the Wave Accounting GraphQL API. You build integrations for small business accounting — managing customers, invoices, payments, products, and financial repo

Quick Summary24 lines
You are a senior developer integrating with the Wave Accounting GraphQL API. You build integrations for small business accounting — managing customers, invoices, payments, products, and financial reports using Wave's GraphQL endpoint with OAuth 2.0 authentication.

## Key Points

1. **Check `didSucceed` on every mutation** — Wave returns `didSucceed: false` with `inputErrors` instead of throwing. Always validate.
2. **Use pagination** — All list queries support `page` and `pageSize`. Default page size is 10.
3. **Request only needed fields** — GraphQL lets you minimize payload. Don't select `*` — pick the fields you need.
4. **Use `externalId` for idempotency** — Pass your own unique ID to prevent duplicate transactions.
5. **Handle currency properly** — Amounts are strings in Wave. Use a decimal library for arithmetic.
- **GraphQL errors return HTTP 200** — Check `data.errors` array even on successful HTTP responses.
- **Wave's API has rate limits** — 100 requests per minute. Implement backoff.
- **Invoice amounts are computed** — You set unit prices and quantities; Wave computes totals. Don't try to set total directly.
- **Product IDs must exist** — Create products first before referencing them in invoice line items.
- **Making separate queries for each field** — Combine into a single GraphQL query.
- **Ignoring `inputErrors` structure** — The `path` field tells you exactly which input field caused the error.
- **Not using variables in GraphQL** — Always use parameterized queries, never string interpolation. Prevents injection.

## Quick Example

```bash
npm install graphql-request graphql
```
skilldb get accounting-software-skills/Wave Accounting GraphQL APIFull skill: 254 lines
Paste into your CLAUDE.md or agent config

Wave Accounting GraphQL API

You are a senior developer integrating with the Wave Accounting GraphQL API. You build integrations for small business accounting — managing customers, invoices, payments, products, and financial reports using Wave's GraphQL endpoint with OAuth 2.0 authentication.

Core Philosophy

GraphQL-First Design

Wave's API is entirely GraphQL — no REST endpoints. You define exactly what data you need in each query, reducing over-fetching. Mutations return the modified object so you always have the latest state.

Free Platform, API-Driven

Wave is free accounting software that monetizes through payment processing. The API lets you automate everything the UI does. This makes it ideal for small business integrations where cost matters.

Business-Scoped Operations

Every operation is scoped to a businessId. A Wave user can have multiple businesses. Always resolve the correct business before making API calls.

Setup

Dependencies

npm install graphql-request graphql

OAuth 2.0 Flow

import axios from 'axios';

const WAVE_AUTH_URL = 'https://api.waveapps.com/oauth2/authorize/';
const WAVE_TOKEN_URL = 'https://api.waveapps.com/oauth2/token/';
const WAVE_GRAPHQL_URL = 'https://gql.waveapps.com/graphql/public';

function getAuthUrl(): string {
  const params = new URLSearchParams({
    client_id: process.env.WAVE_CLIENT_ID!,
    response_type: 'code',
    redirect_uri: 'https://yourapp.com/callback',
    scope: 'account:*',
  });
  return `${WAVE_AUTH_URL}?${params}`;
}

async function exchangeCode(code: string) {
  const response = await axios.post(WAVE_TOKEN_URL, new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: process.env.WAVE_CLIENT_ID!,
    client_secret: process.env.WAVE_CLIENT_SECRET!,
    code,
    redirect_uri: 'https://yourapp.com/callback',
  }));
  return response.data;
}

GraphQL Client Setup

import { GraphQLClient } from 'graphql-request';

function createWaveClient(accessToken: string): GraphQLClient {
  return new GraphQLClient(WAVE_GRAPHQL_URL, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
}

Key Techniques

1. Listing Businesses

async function listBusinesses(client: GraphQLClient) {
  const query = `
    query {
      businesses(page: 1, pageSize: 10) {
        edges {
          node {
            id
            name
            currency { code }
            address {
              city
              province { name }
              country { name }
            }
          }
        }
      }
    }
  `;
  const data = await client.request(query);
  return data.businesses.edges.map((e: any) => e.node);
}

2. Creating Customers

async function createCustomer(client: GraphQLClient, businessId: string) {
  const mutation = `
    mutation ($input: CustomerCreateInput!) {
      customerCreate(input: $input) {
        didSucceed
        inputErrors { path message code }
        customer {
          id
          name
          email
        }
      }
    }
  `;
  const variables = {
    input: {
      businessId,
      name: 'Acme Corporation',
      firstName: 'John',
      lastName: 'Doe',
      email: 'john@acme.com',
      currency: 'USD',
      address: {
        addressLine1: '123 Main St',
        city: 'Austin',
        postalCode: '73301',
        countryCode: 'US',
      },
    },
  };
  const data = await client.request(mutation, variables);
  if (!data.customerCreate.didSucceed) {
    throw new Error(JSON.stringify(data.customerCreate.inputErrors));
  }
  return data.customerCreate.customer;
}

3. Creating Invoices

async function createInvoice(client: GraphQLClient, businessId: string, customerId: string) {
  const mutation = `
    mutation ($input: InvoiceCreateInput!) {
      invoiceCreate(input: $input) {
        didSucceed
        inputErrors { path message code }
        invoice {
          id
          invoiceNumber
          totalAmount { value currency { code } }
          status
        }
      }
    }
  `;
  const variables = {
    input: {
      businessId,
      customerId,
      invoiceDate: '2026-03-25',
      dueDate: '2026-04-25',
      currency: 'USD',
      items: [
        {
          productId: 'product-uuid',
          description: 'Consulting — March 2026',
          quantity: 10,
          unitPrice: '200.00',
        },
      ],
      memo: 'Thank you for your business',
      footer: 'Net 30 terms',
    },
  };
  const data = await client.request(mutation, variables);
  return data.invoiceCreate.invoice;
}

4. Recording Payments

async function createPayment(client: GraphQLClient, invoiceId: string) {
  const mutation = `
    mutation ($input: MoneyTransactionCreateInput!) {
      moneyTransactionCreate(input: $input) {
        didSucceed
        inputErrors { path message code }
        transaction { id }
      }
    }
  `;
  // Note: Wave payment recording is done through money transactions
  // linked to the invoice's business account
  const data = await client.request(mutation, {
    input: {
      businessId: 'business-uuid',
      externalId: 'payment-123',
      date: '2026-03-25',
      description: 'Payment for INV-0042',
      anchor: { accountId: 'income-account-id', amount: '2000.00', direction: 'DEPOSIT' },
      lineItems: [
        { accountId: 'accounts-receivable-id', amount: '2000.00', direction: 'DEBIT' },
      ],
    },
  });
  return data.moneyTransactionCreate;
}

5. Fetching Reports

async function getAccountBalances(client: GraphQLClient, businessId: string) {
  const query = `
    query ($businessId: ID!, $page: Int!) {
      business(id: $businessId) {
        accounts(page: $page, pageSize: 50) {
          edges {
            node {
              id
              name
              type { name value }
              subtype { name value }
              balance
              currency { code }
            }
          }
        }
      }
    }
  `;
  const data = await client.request(query, { businessId, page: 1 });
  return data.business.accounts.edges.map((e: any) => e.node);
}

Best Practices

  1. Check didSucceed on every mutation — Wave returns didSucceed: false with inputErrors instead of throwing. Always validate.
  2. Use pagination — All list queries support page and pageSize. Default page size is 10.
  3. Request only needed fields — GraphQL lets you minimize payload. Don't select * — pick the fields you need.
  4. Use externalId for idempotency — Pass your own unique ID to prevent duplicate transactions.
  5. Handle currency properly — Amounts are strings in Wave. Use a decimal library for arithmetic.

Common Pitfalls

  • GraphQL errors return HTTP 200 — Check data.errors array even on successful HTTP responses.
  • Wave's API has rate limits — 100 requests per minute. Implement backoff.
  • Invoice amounts are computed — You set unit prices and quantities; Wave computes totals. Don't try to set total directly.
  • Product IDs must exist — Create products first before referencing them in invoice line items.

Anti-Patterns

  • Making separate queries for each field — Combine into a single GraphQL query.
  • Ignoring inputErrors structure — The path field tells you exactly which input field caused the error.
  • Not using variables in GraphQL — Always use parameterized queries, never string interpolation. Prevents injection.
  • Treating Wave IDs as integers — Wave uses UUID strings for all entity IDs.

Install this skill directly: skilldb add accounting-software-skills

Get CLI access →

Related Skills