wave
Wave accounting — invoices, customers, transactions, accounts, products, taxes. Small business accounting CLI.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-wave
Repository
Skill path: skills/aiwithabidi/wave
Wave accounting — invoices, customers, transactions, accounts, products, taxes. Small business accounting CLI.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: MIT.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install wave into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding wave to shared team environments
- Use wave for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: wave
description: "Wave accounting — invoices, customers, transactions, accounts, products, taxes. Small business accounting CLI."
homepage: https://www.agxntsix.ai
license: MIT
compatibility: Python 3.10+ (stdlib only — no dependencies)
metadata: {"openclaw": {"emoji": "🌊", "requires": {"env": ["WAVE_API_TOKEN"]}, "primaryEnv": "WAVE_API_TOKEN", "homepage": "https://www.agxntsix.ai"}}
---
# 🌊 Wave
Invoicing and accounting for small business — invoices, customers, transactions.
## Features
- **Businesses** — list connected businesses
- **Invoices** — create, send, list, delete
- **Customers** — manage customer records
- **Accounts** — chart of accounts
- **Transactions** — view financial transactions
- **Products & taxes** — manage items and tax rates
## Requirements
| Variable | Required | Description |
|----------|----------|-------------|
| `WAVE_API_TOKEN` | ✅ | API key/token for Wave |
## Quick Start
```bash
python3 {baseDir}/scripts/wave.py businesses
python3 {baseDir}/scripts/wave.py invoices <business-id>
python3 {baseDir}/scripts/wave.py invoice-create <business-id> <customer-id> --amount 500
python3 {baseDir}/scripts/wave.py customers <business-id>
python3 {baseDir}/scripts/wave.py transactions <business-id>
```
## Credits
Built by [M. Abidi](https://www.linkedin.com/in/mohammad-ali-abidi) | [agxntsix.ai](https://www.agxntsix.ai)
[YouTube](https://youtube.com/@aiwithabidi) | [GitHub](https://github.com/aiwithabidi)
Part of the **AgxntSix Skill Suite** for OpenClaw agents.
📅 **Need help setting up OpenClaw for your business?** [Book a free consultation](https://cal.com/agxntsix/abidi-openclaw)
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "aiwithabidi",
"slug": "wave",
"displayName": "Wave",
"latest": {
"version": "1.0.0",
"publishedAt": 1772931157504,
"commit": "https://github.com/openclaw/skills/commit/e03daa559d199c8e6011319a9ce7fd0aed4e39af"
},
"history": []
}
```
### scripts/wave.py
```python
#!/usr/bin/env python3
"""Wave API CLI. Zero dependencies beyond Python stdlib."""
import argparse, json, os, sys, urllib.request, urllib.error, urllib.parse
def get_token():
token = os.environ.get("WAVE_API_TOKEN", "")
if not token:
env_path = os.path.join(os.environ.get("WORKSPACE", os.path.expanduser("~/.openclaw/workspace")), ".env")
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
line = line.strip()
if line.startswith("WAVE_API_TOKEN="):
token = line.split("=", 1)[1].strip().strip('"').strip("'")
if not token:
print("Error: WAVE_API_TOKEN not set", file=sys.stderr)
sys.exit(1)
return token
API_URL = "https://gql.waveapps.com/graphql/public"
def gql(query, variables=None):
data = {"query": query}
if variables: data["variables"] = variables
body = json.dumps(data).encode()
req = urllib.request.Request(API_URL, data=body, method="POST")
req.add_header("Authorization", f"Bearer {get_token()}")
req.add_header("Content-Type", "application/json")
try:
resp = urllib.request.urlopen(req, timeout=30)
result = json.loads(resp.read().decode())
if result.get("errors"): print(f"GQL Error: {result['errors']}", file=sys.stderr); sys.exit(1)
return result.get("data", {})
except urllib.error.HTTPError as e:
print(f"API Error {e.code}: {e.read().decode()}", file=sys.stderr); sys.exit(1)
def cmd_businesses(a):
d = gql("query{businesses{edges{node{id name isPersonal currency{code}}}}}")
for e in d.get("businesses",{}).get("edges",[]): print(json.dumps(e["node"]))
def cmd_invoices(a):
d = gql("query($bid:ID!,$page:Int,$size:Int){business(id:$bid){invoices(page:$page,pageSize:$size){edges{node{id title status total{value currency{code}}customer{name}invoiceDate dueDate}}}}}", {"bid":a.business_id,"page":a.page,"size":a.limit})
for e in d.get("business",{}).get("invoices",{}).get("edges",[]): print(json.dumps(e["node"]))
def cmd_invoice_get(a):
d = gql("query($bid:ID!,$iid:ID!){business(id:$bid){invoice(id:$iid){id title status total{value}customer{name}items{description quantity unitPrice{value}}invoiceDate dueDate pdfUrl}}}", {"bid":a.business_id,"iid":a.id})
print(json.dumps(d.get("business",{}).get("invoice",{}), indent=2))
def cmd_invoice_create(a):
items = json.loads(a.items) if a.items else [{"description":"Service","unitPrice":a.amount or "0","quantity":1}]
d = gql("mutation($input:InvoiceCreateInput!){invoiceCreate(input:$input){invoice{id title status viewUrl}didSucceed inputErrors{path message}}}", {"input":{"businessId":a.business_id,"customerId":a.customer_id,"items":items}})
print(json.dumps(d.get("invoiceCreate",{}), indent=2))
def cmd_invoice_send(a):
d = gql("mutation($input:InvoiceSendInput!){invoiceSend(input:$input){didSucceed inputErrors{message}}}", {"input":{"invoiceId":a.id}})
print(json.dumps(d.get("invoiceSend",{}), indent=2))
def cmd_invoice_delete(a):
d = gql("mutation($input:InvoiceDeleteInput!){invoiceDelete(input:$input){didSucceed}}", {"input":{"invoiceId":a.id}})
print(json.dumps(d.get("invoiceDelete",{})))
def cmd_customers(a):
d = gql("query($bid:ID!,$page:Int,$size:Int){business(id:$bid){customers(page:$page,pageSize:$size){edges{node{id name email}}}}}",{"bid":a.business_id,"page":1,"size":a.limit})
for e in d.get("business",{}).get("customers",{}).get("edges",[]): print(json.dumps(e["node"]))
def cmd_customer_create(a):
d = gql("mutation($input:CustomerCreateInput!){customerCreate(input:$input){customer{id name email}didSucceed inputErrors{message}}}", {"input":{"businessId":a.business_id,"name":a.name,"email":a.email}})
print(json.dumps(d.get("customerCreate",{}), indent=2))
def cmd_accounts(a):
d = gql("query($bid:ID!){business(id:$bid){accounts{edges{node{id name type{value}subtype{value}currency{code}}}}}}",{"bid":a.business_id})
for e in d.get("business",{}).get("accounts",{}).get("edges",[]): print(json.dumps(e["node"]))
def cmd_transactions(a):
d = gql("query($bid:ID!,$page:Int,$size:Int){business(id:$bid){transactions(page:$page,pageSize:$size){edges{node{id description date amount{value currency{code}}account{name}}}}}}",{"bid":a.business_id,"page":a.page,"size":a.limit})
for e in d.get("business",{}).get("transactions",{}).get("edges",[]): print(json.dumps(e["node"]))
def cmd_products(a):
d = gql("query($bid:ID!,$page:Int,$size:Int){business(id:$bid){products(page:$page,pageSize:$size){edges{node{id name description unitPrice}}}}}",{"bid":a.business_id,"page":1,"size":a.limit})
for e in d.get("business",{}).get("products",{}).get("edges",[]): print(json.dumps(e["node"]))
def cmd_taxes(a):
d = gql("query($bid:ID!){business(id:$bid){salesTaxes{edges{node{id name rate}}}}}",{"bid":a.business_id})
for e in d.get("business",{}).get("salesTaxes",{}).get("edges",[]): print(json.dumps(e["node"]))
def main():
p = argparse.ArgumentParser(description="Wave Accounting CLI")
s = p.add_subparsers(dest="command")
s.add_parser("businesses")
x = s.add_parser("invoices"); x.add_argument("business_id"); x.add_argument("--limit", type=int, default=50); x.add_argument("--page", type=int, default=1)
x = s.add_parser("invoice"); x.add_argument("business_id"); x.add_argument("id")
x = s.add_parser("invoice-create"); x.add_argument("business_id"); x.add_argument("customer_id"); x.add_argument("--items"); x.add_argument("--amount")
x = s.add_parser("invoice-send"); x.add_argument("id")
x = s.add_parser("invoice-delete"); x.add_argument("id")
x = s.add_parser("customers"); x.add_argument("business_id"); x.add_argument("--limit", type=int, default=50)
x = s.add_parser("customer-create"); x.add_argument("business_id"); x.add_argument("name"); x.add_argument("email")
x = s.add_parser("accounts"); x.add_argument("business_id")
x = s.add_parser("transactions"); x.add_argument("business_id"); x.add_argument("--limit", type=int, default=50); x.add_argument("--page", type=int, default=1)
x = s.add_parser("products"); x.add_argument("business_id"); x.add_argument("--limit", type=int, default=50)
x = s.add_parser("taxes"); x.add_argument("business_id")
a = p.parse_args()
c = {"businesses":cmd_businesses,"invoices":cmd_invoices,"invoice":cmd_invoice_get,"invoice-create":cmd_invoice_create,"invoice-send":cmd_invoice_send,"invoice-delete":cmd_invoice_delete,"customers":cmd_customers,"customer-create":cmd_customer_create,"accounts":cmd_accounts,"transactions":cmd_transactions,"products":cmd_products,"taxes":cmd_taxes}
if a.command in c: c[a.command](a)
else: p.print_help()
if __name__ == "__main__": main()
```