Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Wave

To integrate Replit with Wave Accounting, create an app at developer.waveapps.com, implement OAuth 2.0 to get a user access token, store it in Replit Secrets (lock icon πŸ”’), and query Wave's GraphQL API at graphql.waveapps.com/graphql/public. Wave uses GraphQL (not REST) for all operations β€” you write queries and mutations to read invoices, create customers, and record payments.

What you'll learn

  • How to create a Wave developer app and implement the OAuth 2.0 authorization flow
  • How to send GraphQL queries and mutations to Wave's API from Node.js and Python
  • How to create and send invoices, add customers, and record payments via Wave API
  • How to handle Wave's GraphQL error format and partial success responses
  • The difference between Wave's free GraphQL API and paid accounting platforms
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read25 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with Wave Accounting, create an app at developer.waveapps.com, implement OAuth 2.0 to get a user access token, store it in Replit Secrets (lock icon πŸ”’), and query Wave's GraphQL API at graphql.waveapps.com/graphql/public. Wave uses GraphQL (not REST) for all operations β€” you write queries and mutations to read invoices, create customers, and record payments.

Wave Accounting GraphQL API from Replit

Wave is a free accounting platform popular with freelancers and small businesses. Its core features β€” invoicing, expense tracking, receipt scanning, and basic financial reporting β€” are free, making it one of the most accessible accounting tools for bootstrapped businesses. Wave provides a GraphQL API that allows programmatic access to most of these features, enabling integrations for automated invoicing, payment reconciliation, and accounting workflow automation.

Wave's API is GraphQL-only. This means instead of REST endpoints for each resource type (GET /invoices, POST /customers), you write queries and mutations against a single endpoint. A query retrieves data and a mutation creates or modifies data. GraphQL's schema is self-documenting β€” you can inspect available operations and their required fields using the Wave API Explorer at developer.waveapps.com/graphql/explorer.

OAuth 2.0 handles user authentication β€” your app redirects the Wave user to authorize access, receives an authorization code, exchanges it for an access token, and includes that token in all subsequent GraphQL requests. For server-side automations where you control the Wave account, you run the OAuth flow once, save the resulting access token (and optionally refresh token), and use it for all API calls until it expires.

Integration method

Standard API Integration

Wave's API uses GraphQL with OAuth 2.0 authentication. Your Replit server implements the OAuth 2.0 authorization code flow to obtain a user access token, stores it in Replit Secrets, and sends GraphQL queries and mutations to wave's API endpoint. Unlike REST APIs, all Wave operations go through POST requests to a single GraphQL endpoint with operation-specific query strings.

Prerequisites

  • A Replit account with a Node.js or Python Repl ready
  • A Wave account at waveapps.com (free to create)
  • A Wave developer app registered at developer.waveapps.com
  • Familiarity with GraphQL queries and mutations (optional but helpful)

Step-by-step guide

1

Create a Wave Developer App

Navigate to developer.waveapps.com and sign in with your Wave account. Click 'Create App'. Give your app a name (e.g., Replit Integration) and provide a description. For the Redirect URL, enter the URL of your Replit app's OAuth callback endpoint β€” for example, https://yourapp.yourusername.repl.co/auth/callback. After creating the app, Wave shows you the Client ID and Client Secret. Copy both immediately β€” the client secret cannot be retrieved again after you leave the page. Wave's OAuth scopes define what your app can access. Available scopes include: account (read business account data), business:read and business:write (read/write business financial data), and user (read user profile). For invoicing and accounting operations, you need business:read and business:write. Wave's OAuth 2.0 flow uses standard authorization code flow: redirect the user to https://api.waveapps.com/oauth2/authorize with your client_id, redirect_uri, scope, and a state parameter. After user authorization, Wave redirects to your callback URL with a code parameter. Exchange this code at https://api.waveapps.com/oauth2/token for an access token. For a single-business integration (your own Wave account), run the OAuth flow once in your browser, copy the resulting access_token, and store it in Replit Secrets. The access token does not expire quickly β€” it is valid until revoked. This is simpler than implementing full OAuth in your server for personal automations.

Pro tip: Wave's access tokens are long-lived (they do not expire on a short timer like Stripe or Salesforce tokens). For personal automation tools, running the OAuth flow once manually and storing the resulting token in Replit Secrets is a pragmatic approach.

Expected result: A Wave developer app exists with Client ID and Client Secret. The OAuth redirect URL points to your Replit app. An access token has been obtained via OAuth flow.

2

Store Wave Credentials in Replit Secrets

Click the lock icon (πŸ”’) in the left Replit sidebar to open the Secrets pane. Add the following secrets: WAVE_CLIENT_ID: your Wave app's Client ID. WAVE_CLIENT_SECRET: your Wave app's Client Secret. WAVE_ACCESS_TOKEN: the OAuth access token obtained after authorization. WAVE_REDIRECT_URI: your OAuth callback URL (must match what was registered in the Wave app). The WAVE_ACCESS_TOKEN is the key credential for all GraphQL API calls. Include it in the Authorization: Bearer header of every request to the Wave GraphQL endpoint (https://gql.waveapps.com/graphql/public). For multi-user applications where different Wave accounts authorize your app, store tokens in a database keyed by user ID rather than as environment variables. For single-user tools (your own Wave account), storing in Replit Secrets is fine. Wave's client secret should be treated as highly sensitive β€” it can be used to exchange OAuth codes for access tokens on behalf of any user who authorizes your app.

check-wave-secrets.js
1// check-wave-secrets.js
2const required = ['WAVE_CLIENT_ID', 'WAVE_CLIENT_SECRET', 'WAVE_ACCESS_TOKEN', 'WAVE_REDIRECT_URI'];
3for (const key of required) {
4 if (!process.env[key]) {
5 throw new Error(`Missing secret: ${key}. Set it in Replit Secrets (lock icon πŸ”’).`);
6 }
7}
8console.log('Wave secrets verified.');
9console.log('Client ID:', process.env.WAVE_CLIENT_ID);

Pro tip: Test your access token immediately after storing it by making a simple query: query { user { id defaultEmail } }. This confirms the token is valid and your GraphQL requests are properly authenticated.

Expected result: All four Wave secrets appear in Replit Secrets. The check script prints the Client ID without errors.

3

Query and Mutate Wave Data with GraphQL (Node.js)

Install required packages in the Shell tab: npm install axios express. All Wave API operations go through a single endpoint: POST https://gql.waveapps.com/graphql/public. The request body contains a query string (GraphQL operation) and optionally a variables object for parameterized queries. To get started, query your businesses β€” the business ID (UUID) is required for all business-specific operations like creating invoices. Then query customers (customers in Wave are called 'customers' in the API). Creating invoices requires the business ID, customer ID, and an array of line items. Wave's GraphQL schema is available at developer.waveapps.com/graphql/explorer β€” use it to browse available queries and mutations, see required and optional fields, and test operations before writing code. Error handling in GraphQL differs from REST. GraphQL always returns HTTP 200, even for errors. Check the errors array in the response body for error messages. Also check for data-level errors in mutation responses β€” Wave's mutations often return a didSucceed boolean alongside the created/updated resource. Important Wave concepts: invoiceItems are line items on an invoice. A product or service in Wave is called an 'income account'. Dates must be in YYYY-MM-DD format. Amounts are in the business's currency as decimal numbers.

wave.js
1// wave.js β€” Wave Accounting GraphQL API for Node.js on Replit
2const axios = require('axios');
3const express = require('express');
4
5const app = express();
6app.use(express.json());
7
8const WAVE_GQL = 'https://gql.waveapps.com/graphql/public';
9const TOKEN = process.env.WAVE_ACCESS_TOKEN;
10
11async function waveQuery(query, variables = {}) {
12 const response = await axios.post(
13 WAVE_GQL,
14 { query, variables },
15 { headers: { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json' } }
16 );
17 if (response.data.errors) {
18 throw new Error(JSON.stringify(response.data.errors));
19 }
20 return response.data.data;
21}
22
23// Get all businesses on the account
24app.get('/api/businesses', async (req, res) => {
25 try {
26 const data = await waveQuery(`
27 query GetBusinesses {
28 businesses {
29 edges {
30 node {
31 id
32 name
33 currency { code symbol }
34 isPersonal
35 }
36 }
37 }
38 }
39 `);
40 const businesses = data.businesses.edges.map(e => e.node);
41 res.json({ businesses });
42 } catch (err) {
43 res.status(500).json({ error: err.message });
44 }
45});
46
47// Get customers for a business
48app.get('/api/customers/:businessId', async (req, res) => {
49 try {
50 const data = await waveQuery(`
51 query GetCustomers($businessId: ID!) {
52 business(id: $businessId) {
53 customers {
54 edges {
55 node {
56 id
57 name
58 email
59 mobile
60 }
61 }
62 }
63 }
64 }
65 `, { businessId: req.params.businessId });
66
67 const customers = data.business.customers.edges.map(e => e.node);
68 res.json({ customers });
69 } catch (err) {
70 res.status(500).json({ error: err.message });
71 }
72});
73
74// Create an invoice
75app.post('/api/invoices', async (req, res) => {
76 const { businessId, customerId, invoiceDate, dueDate, items, memo } = req.body;
77 try {
78 const data = await waveQuery(`
79 mutation CreateInvoice($input: InvoiceCreateInput!) {
80 invoiceCreate(input: $input) {
81 didSucceed
82 inputErrors { code message path }
83 invoice {
84 id
85 invoiceNumber
86 status
87 amountDue { value currency { code } }
88 pdfUrl
89 }
90 }
91 }
92 `, {
93 input: {
94 businessId,
95 customerId,
96 invoiceDate,
97 dueDate,
98 memo: memo || '',
99 items: items.map(item => ({
100 productId: item.productId,
101 description: item.description,
102 quantity: item.quantity,
103 unitPrice: item.unitPrice
104 }))
105 }
106 });
107
108 const result = data.invoiceCreate;
109 if (!result.didSucceed) {
110 return res.status(400).json({ errors: result.inputErrors });
111 }
112 res.json({ success: true, invoice: result.invoice });
113 } catch (err) {
114 res.status(500).json({ error: err.message });
115 }
116});
117
118app.listen(3000, '0.0.0.0', () => console.log('Wave API server running on port 3000'));

Pro tip: Wave mutations always return a didSucceed field alongside inputErrors. Always check didSucceed before assuming the operation succeeded β€” GraphQL returns HTTP 200 even for failed mutations, so checking the HTTP status alone is not sufficient.

Expected result: GET /api/businesses returns your Wave account's businesses. GET /api/customers/{businessId} returns customers. POST /api/invoices creates an invoice and returns the Wave invoice ID and PDF URL.

4

Python Integration for Wave GraphQL API

For Python Replit projects, install requests and flask: pip install requests flask. GraphQL requests in Python are simple β€” they are just POST requests with a JSON body containing query and variables keys. Create a helper function that constructs the POST request with the Bearer token header, sends the query, checks for GraphQL errors in the response, and returns the data. This single function handles all Wave operations. For creating customers, use the customerCreate mutation with required fields: businessId, name, and optionally email, mobile, and address fields. For recording payments against invoices, use the invoicePaymentCreate mutation with invoiceId, paymentDate, amount, and accountId (the Wave income account where the payment is recorded). For scheduled reporting jobs, build a Python script that queries Wave invoices with a date range filter, calculates totals and aging buckets, and outputs a report. Deploy this as a Reserved VM on Replit if you want it to run automatically on a schedule.

wave_api.py
1# wave_api.py β€” Wave Accounting GraphQL API for Python on Replit
2import os
3import requests
4from flask import Flask, request, jsonify
5
6WAVE_GQL = 'https://gql.waveapps.com/graphql/public'
7TOKEN = os.environ['WAVE_ACCESS_TOKEN']
8
9session = requests.Session()
10session.headers.update({'Authorization': f'Bearer {TOKEN}', 'Content-Type': 'application/json'})
11
12app = Flask(__name__)
13
14def wave_query(query: str, variables: dict = None) -> dict:
15 """Execute a Wave GraphQL query or mutation."""
16 response = session.post(WAVE_GQL, json={'query': query, 'variables': variables or {}})
17 response.raise_for_status()
18 data = response.json()
19 if 'errors' in data:
20 raise ValueError(f"GraphQL errors: {data['errors']}")
21 return data['data']
22
23@app.route('/api/businesses')
24def get_businesses():
25 try:
26 data = wave_query('''
27 query { businesses { edges { node { id name currency { code } } } } }
28 ''')
29 businesses = [e['node'] for e in data['businesses']['edges']]
30 return jsonify({'businesses': businesses})
31 except Exception as e:
32 return jsonify({'error': str(e)}), 500
33
34@app.route('/api/invoices')
35def get_invoices():
36 business_id = request.args.get('businessId')
37 if not business_id:
38 return jsonify({'error': 'businessId required'}), 400
39 try:
40 data = wave_query('''
41 query GetInvoices($businessId: ID!) {
42 business(id: $businessId) {
43 invoices {
44 edges {
45 node {
46 id invoiceNumber status
47 invoiceDate dueDate
48 amountDue { value }
49 customer { id name email }
50 }
51 }
52 }
53 }
54 }
55 ''', {'businessId': business_id})
56 invoices = [e['node'] for e in data['business']['invoices']['edges']]
57 return jsonify({'invoices': invoices, 'count': len(invoices)})
58 except Exception as e:
59 return jsonify({'error': str(e)}), 500
60
61@app.route('/api/customers', methods=['POST'])
62def create_customer():
63 payload = request.get_json()
64 try:
65 data = wave_query('''
66 mutation CreateCustomer($input: CustomerCreateInput!) {
67 customerCreate(input: $input) {
68 didSucceed
69 inputErrors { code message }
70 customer { id name email }
71 }
72 }
73 ''', {'input': {
74 'businessId': payload['businessId'],
75 'name': payload['name'],
76 'email': payload.get('email', '')
77 }})
78 result = data['customerCreate']
79 if not result['didSucceed']:
80 return jsonify({'errors': result['inputErrors']}), 400
81 return jsonify({'success': True, 'customer': result['customer']})
82 except Exception as e:
83 return jsonify({'error': str(e)}), 500
84
85if __name__ == '__main__':
86 app.run(host='0.0.0.0', port=3000)

Pro tip: Wave's GraphQL API requires the full business ID UUID in most mutations. Get this from the businesses query first, then store it as WAVE_BUSINESS_ID in Replit Secrets so you do not need to query it on every request.

Expected result: GET /api/businesses returns Wave businesses. GET /api/invoices?businessId={id} returns invoices. POST /api/customers creates a customer and returns the Wave customer ID.

Common use cases

Automated Invoice Generation

Automatically create and send Wave invoices when orders are placed or services are delivered. Read customer data from your app's database, create the invoice in Wave via the API with line items and due dates, and trigger sending. Eliminate manual invoice creation for recurring billing scenarios.

Replit Prompt

Build a Node.js endpoint that accepts customer ID, line items, and due date, creates an invoice in Wave via GraphQL mutation, and returns the invoice URL and Wave invoice ID.

Copy this prompt to try it in Replit

Payment Reconciliation

Match incoming payments from Stripe or bank transactions against Wave invoices. When a payment is received, query Wave for the corresponding invoice, record the payment on the invoice via the API, and mark it as paid. Maintain accurate books without manually updating Wave for each transaction.

Replit Prompt

Create a webhook receiver that accepts a payment notification, looks up the corresponding Wave invoice by customer email, records the payment amount and date via Wave's GraphQL API, and returns the updated invoice status.

Copy this prompt to try it in Replit

Financial Reporting Dashboard

Pull Wave accounting data to build custom financial dashboards beyond Wave's native reports. Query invoices, expenses, and payment data to calculate metrics like monthly recurring revenue, overdue accounts receivable aging, or expense-to-revenue ratios over custom time periods.

Replit Prompt

Build a reporting endpoint that queries Wave for all invoices from the last 12 months, calculates monthly revenue, outstanding balances by customer, and returns a JSON summary for a financial dashboard.

Copy this prompt to try it in Replit

Troubleshooting

GraphQL returns errors array with 'Unauthorized' or 'Authentication required'

Cause: The WAVE_ACCESS_TOKEN in Replit Secrets is missing, invalid, or expired. Also occurs if the token does not have the required scopes for the operation.

Solution: Verify WAVE_ACCESS_TOKEN is set in Replit Secrets and contains the correct token. Test the token by querying { user { id defaultEmail } } β€” if this fails, the token is invalid or expired. Re-run the OAuth flow to get a fresh token.

typescript
1// Quick token validation query
2const data = await waveQuery('{ user { id defaultEmail } }');
3console.log('Token valid. User:', data.user.defaultEmail);

invoiceCreate mutation returns didSucceed: false with inputErrors

Cause: Required fields are missing (customerId, businessId, invoiceDate), a referenced productId does not exist in the business, or date format is wrong.

Solution: Log the full inputErrors array β€” each error has a path (which field failed) and a message. Verify invoiceDate uses YYYY-MM-DD format. Confirm that the productId references a valid Wave income account in the business.

typescript
1// Log mutation errors for debugging
2if (!result.didSucceed) {
3 console.error('Wave mutation errors:');
4 result.inputErrors.forEach(e => console.error(` ${e.path}: ${e.message} (${e.code})`));
5}

GraphQL query returns null for business data even with valid token

Cause: The business ID is incorrect, or the authorized user does not have access to that business. Wave's OAuth tokens are user-scoped and only provide access to businesses the user has access to.

Solution: Query { businesses { edges { node { id name } } } } to list all accessible businesses and verify the business ID. Make sure the OAuth token was obtained from an account that has access to the target business.

OAuth token exchange fails β€” invalid_grant error

Cause: The authorization code has expired (codes expire in minutes), the redirect_uri in the token exchange does not match the one used in the authorization redirect, or the code was already used.

Solution: Complete the OAuth flow quickly after initiating it. Verify WAVE_REDIRECT_URI matches exactly what is registered in your Wave app settings. Authorization codes are single-use β€” if token exchange fails, restart the OAuth flow from the authorization redirect.

Best practices

  • Store WAVE_CLIENT_ID, WAVE_CLIENT_SECRET, WAVE_ACCESS_TOKEN, and WAVE_REDIRECT_URI in Replit Secrets (lock icon πŸ”’) β€” never expose OAuth credentials or access tokens in code
  • Always check the didSucceed field in Wave mutations β€” GraphQL returns HTTP 200 even for failed mutations, so HTTP status alone is not sufficient for error detection
  • Store your Wave business ID as WAVE_BUSINESS_ID in Replit Secrets after initial setup β€” this avoids querying the businesses list on every operation
  • Use Wave's GraphQL Explorer at developer.waveapps.com/graphql/explorer to prototype queries before implementing them in code
  • Log mutation inputErrors with the full path and message for debugging β€” Wave's error messages are specific and clearly identify which field caused the failure
  • For multi-user apps, store OAuth tokens in a database keyed by user ID rather than as environment variables
  • Deploy as Autoscale for on-demand invoice and payment operations; use Reserved VM for scheduled financial reporting jobs
  • Request only the OAuth scopes your integration needs β€” business:write if you only need to create invoices; business:read if you are building read-only reports

Alternatives

Frequently asked questions

How do I connect Replit to Wave Accounting?

Create a Wave developer app at developer.waveapps.com to get a Client ID and Secret, implement OAuth 2.0 to obtain an access token, store it in Replit Secrets as WAVE_ACCESS_TOKEN, and send GraphQL queries and mutations to https://gql.waveapps.com/graphql/public with Authorization: Bearer {token} headers.

Does Wave use REST or GraphQL?

Wave uses GraphQL exclusively. All API operations β€” reading invoices, creating customers, recording payments β€” are sent as POST requests to a single GraphQL endpoint. The request body contains a query or mutation string and a variables object. There are no REST-style /invoices or /customers endpoints.

Is Wave's API free to use?

Yes. Wave's core accounting features are free, and the API access is also free with a registered developer app. There are no per-API-call fees. Wave makes money from its payment processing add-on, not from API access.

How do I store my Wave API credentials in Replit?

Click the lock icon (πŸ”’) in the Replit sidebar and add WAVE_CLIENT_ID, WAVE_CLIENT_SECRET, WAVE_ACCESS_TOKEN, and WAVE_REDIRECT_URI. The access token is the most important β€” include it as Authorization: Bearer {token} in all GraphQL requests. Access it with process.env.WAVE_ACCESS_TOKEN (Node.js) or os.environ['WAVE_ACCESS_TOKEN'] (Python).

What deployment type should I use on Replit for Wave integrations?

Use Autoscale for on-demand invoice creation and payment recording triggered by user actions. Use Reserved VM for scheduled financial reporting jobs that run at the end of each month or billing cycle. Most Wave integrations are event-driven (invoice on sale, pay on approval), which suits Autoscale's stateless execution model.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation β€” no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.