← All Skills

snappy-freshbooks

v1.0.0
22 files, 211.5 KB ~5,930 words · 24 min read Updated 2026-09-09

snappy-freshbooks skill

48 of 54 checks pass
What it can do
clientsread
invoicesread
invoice invoice_idread
listread
get invoice_idread
time-entriesread
expensesread
metrics nameread
create-invoice payloadwrite
update-invoice payloadwrite
send-invoice invoice_idsend
mark-paid payloadpay
+2 more
What does not pass yet
Architecture 2 endpoints
api.freshbooks.com2 endpoints
POST/accounting/account/{id}/invoices/invoices
POST/accounting/account/{id}/payments/payments
$ npx snappy-skills install snappy-freshbooks
zip ↓
File Tree
├── AGENTS.md ├── SKILL.md ├── api-reference.md ├── api.ts ├── entities.json ├── face.test.ts ├── faces/ │ ├── components/ │ │ ├── freshbooks-decision.tsx │ │ ├── freshbooks-invoice-list.css │ │ ├── freshbooks-invoice-list.tsx │ │ ├── freshbooks-invoice-preview.css │ │ └── freshbooks-invoice-preview.tsx │ ├── family.tsx │ └── fixtures/ │ ├── freshbooks-decision.json │ ├── freshbooks-invoice.json │ └── freshbooks-list.json ├── metrics.json ├── read-limit.test.ts ├── recurring-and-expenses.md ├── refusals.test.ts ├── scripts/ │ └── reauth.ts ├── stage.test.ts └── workflows.md
Documents
AGENTS.md

snappy-freshbooks -- Agent Loader#

Single source of truth for everything money-related in Snappy. All operations call the FreshBooks API directly (OAuth2 with refresh token) -- no Xano middleware. Other skills hand off here -- they never duplicate endpoints inline.

CRITICAL RULE: Drafts only, never send#

This skill never sends invoices to clients. It creates and updates DRAFT invoices. Robert (or another human) reviews the draft in the FreshBooks UI and clicks Send there. Any caller that tries to send via sendInvoice or the send-invoice CLI gets a hard error — the function is a refusing stub and the CLI exits with code 2.

The single send path is: agent creates/updates the draft → agent tells Robert the draft is ready → Robert reviews in FreshBooks UI → Robert sends. No exceptions, no "just this once", no bulk batches. This rule exists because an invoice sent wrong is a broken client relationship and there is no undo.

API module#

typescriptimport {
  listClients, getOrCreateClient,
  listInvoices, createInvoice, updateInvoice,
  markPaid,
  listTimeEntries, createTimeEntry,
  listExpenses, createExpense,
} from "../snappy-freshbooks/api.ts";

Or CLI:

bashnpx tsx ~/.claude/skills/snappy-freshbooks/api.ts clients
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoices
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts time-entries
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts expenses
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts create-invoice '{"client_id":1,"lines":[{"name":"Retainer","amount":5000}]}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts update-invoice '{"invoice_id":123,"notes":"updated scope"}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts mark-paid '{"invoice_id":123,"payment_date":"2026-04-08"}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-time '{"client_id":1,"hours":2,"note":"..."}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-expense '{"category":"software_saas","amount":99,"vendor":"..."}'
# send-invoice is disabled by policy — exits with code 2

Credentials loaded via snappy-settings/load.ts from .env.cache. Requires: FRESHBOOKS_CLIENT_ID, FRESHBOOKS_CLIENT_SECRET, FRESHBOOKS_REFRESH_TOKEN, FRESHBOOKS_ACCOUNT_ID.

Key Capabilities#

  • Create DRAFT invoices -- createInvoice(). Always draft (status: 1). Never auto-sends.
  • Update DRAFT invoices -- updateInvoice(). Refuses anything that isn't v3_status === "draft".
  • Recurring retainer batch -- 1st of month, draft all active retainers. Robert reviews and sends in bulk from FreshBooks UI. See workflows.md#2.
  • Time tracking -- createTimeEntry(). Review unbilled hours on Fridays.
  • Expense logging -- createExpense(). Always set category from: software_saas, contractors, ads, tools_infra, professional, travel, office.
  • Payment tracking -- markPaid() records a payment against an invoice Robert already sent. This is bookkeeping, not sending.
  • Overdue follow-up -- Day 7 email, Day 14 WhatsApp, Day 30 escalation. Always dry_run: true first. Robert sends the actual chase message.
  • Revenue dashboard -- MRR, cash flow, profit margin, runway, churn detection.
  • Monthly close -- Last Friday of month: revenue, expenses, profit, runway summary.

FreshBooks API (direct, v3 accounting)#

Base: https://api.freshbooks.com. Auth: OAuth2 refresh token (auto-handled by api.ts).

operation method FreshBooks endpoint
List clients GET /accounting/account/{id}/users/clients
Create client POST /accounting/account/{id}/users/clients
List invoices GET /accounting/account/{id}/invoices/invoices
Create invoice POST /accounting/account/{id}/invoices/invoices
Update invoice PUT /accounting/account/{id}/invoices/invoices/{inv_id}
List time entries GET /accounting/account/{id}/time_entries
Create time entry POST /accounting/account/{id}/time_entries
List expenses GET /accounting/account/{id}/expenses/expenses
Create expense POST /accounting/account/{id}/expenses/expenses
Create payment POST /accounting/account/{id}/payments/payments

Common Mistakes to Avoid#

  • Trying to send. Don't. sendInvoice is a refusing stub and the send-invoice CLI exits with code 2. Draft and tell Robert; he sends from the FreshBooks UI.
  • Updating a non-draft invoice. updateInvoice refuses anything whose v3_status !== "draft". Once Robert has sent it, edits happen in the FreshBooks UI.
  • Assuming creation auto-emails. It never has. Creation makes a draft with status: 1; that's the whole contract.
  • markPaid creates a payment record on an already-sent invoice; it is bookkeeping, not sending.
  • MRR = paid + outstanding recurring THIS month, not historical sent.
  • Overdue chase starts Day 7, not Day 1. Always dry_run: true first.
  • Use getOrCreateClient (idempotent) -- never create-only.

Cadence Hooks (from snappy-ops)#

trigger when workflow
Daily EOD every weekday Log billable time
Friday close every Friday Outstanding invoices + unbilled time + overdue scan
1st of month first business day Recurring retainer batch
Last Friday last business day Monthly close

Targets#

Profit margin >= 60% (red < 50%). Receivables < 1mo expenses. Single client <= 30% MRR. Runway >= 6mo. Days to invoice <= 2.

Files in This Skill#

file purpose
SKILL.md Full skill definition -- endpoints, decision map, cadence hooks, targets
workflows.md 5 core workflows: new invoice, recurring batch, overdue follow-up, revenue dashboard, monthly close
recurring-and-expenses.md Recurring retainers, expense entry, payment tracking, cash flow monitoring
api-reference.md Full endpoint catalog with request/response shapes

Auth#

OAuth2 with refresh token. api.ts handles token refresh automatically -- reads FRESHBOOKS_CLIENT_ID, FRESHBOOKS_CLIENT_SECRET, FRESHBOOKS_REFRESH_TOKEN, FRESHBOOKS_ACCOUNT_ID from .env.cache via snappy-settings/load.ts. Access token cached in memory with expiry tracking.

Re-auth (when refresh token is empty or revoked)#

Run the helper -- it handles the authorization_code flow, auto-discovers FRESHBOOKS_ACCOUNT_ID via /auth/api/v1/users/me, writes both back to .env.cache, then smoke-tests listClients:

bash# Pre-req: register a dev app at https://my.freshbooks.com/#/developer
#          redirect URI: https://api.freshbooks.com/auth/oauth/redirect
#          paste CLIENT_ID + CLIENT_SECRET into .env.cache (manual, one-time).
npx tsx ~/.claude/skills/snappy-freshbooks/scripts/reauth.ts
# -> prints auth URL, you authorize, paste ?code=XXX from redirect, done.

# Verify later without re-auth:
npx tsx ~/.claude/skills/snappy-freshbooks/scripts/reauth.ts --smoke-test

Idempotent. Does NOT touch CLIENT_ID/CLIENT_SECRET -- those are manual.


If this loader doesn't cover your case:

bashecho "[$(date -u +%FT%TZ)] snappy-freshbooks: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log

Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.

<!-- SKILL-INDEX-START -->

[snappy-freshbooks Index]|root: ~/.claude/skills/snappy-freshbooks|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,api-reference.md,recurring-and-expenses.md,workflows.md}

<!-- SKILL-INDEX-END -->

Used by#

  • snappy-slack
  • snappy-telegram
  • snappy-whatsapp

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
clients read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts clients
invoices read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoices
invoice invoice_id read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoice <invoice_id>
list read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts list
get invoice_id read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts get <invoice_id>
time-entries read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts time-entries
expenses read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts expenses
metrics name read npx tsx ~/.claude/skills/snappy-freshbooks/api.ts metrics "<name>"
create-invoice payload write npx tsx ~/.claude/skills/snappy-freshbooks/api.ts create-invoice <payload>
update-invoice payload write npx tsx ~/.claude/skills/snappy-freshbooks/api.ts update-invoice <payload>
send-invoice invoice_id send npx tsx ~/.claude/skills/snappy-freshbooks/api.ts send-invoice <invoice_id>
mark-paid payload pay npx tsx ~/.claude/skills/snappy-freshbooks/api.ts mark-paid <payload>
log-time payload write-reversible npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-time <payload>
log-expense payload write-reversible npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-expense <payload>

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->

---
name: snappy-freshbooks
role: Authoritative source for all FreshBooks operations -- DRAFT invoicing, billing, time tracking, expenses, recurring retainers, payment tracking, dashboards, monthly close, and overdue follow-up.
loaded-by: PreToolUse hook (auto-injected when "snappy-freshbooks" is mentioned)
---

# snappy-freshbooks -- Agent Loader

Single source of truth for everything money-related in Snappy. All operations call the FreshBooks API directly (OAuth2 with refresh token) -- no Xano middleware. Other skills hand off here -- they never duplicate endpoints inline.

## CRITICAL RULE: Drafts only, never send

**This skill never sends invoices to clients.** It creates and updates DRAFT invoices. Robert (or another human) reviews the draft in the FreshBooks UI and clicks Send there. Any caller that tries to send via `sendInvoice` or the `send-invoice` CLI gets a hard error — the function is a refusing stub and the CLI exits with code 2.

The single send path is: agent creates/updates the draft → agent tells Robert the draft is ready → Robert reviews in FreshBooks UI → Robert sends. No exceptions, no "just this once", no bulk batches. This rule exists because an invoice sent wrong is a broken client relationship and there is no undo.

## API module

```typescript
import {
  listClients, getOrCreateClient,
  listInvoices, createInvoice, updateInvoice,
  markPaid,
  listTimeEntries, createTimeEntry,
  listExpenses, createExpense,
} from "../snappy-freshbooks/api.ts";
```

Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts clients
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoices
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts time-entries
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts expenses
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts create-invoice '{"client_id":1,"lines":[{"name":"Retainer","amount":5000}]}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts update-invoice '{"invoice_id":123,"notes":"updated scope"}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts mark-paid '{"invoice_id":123,"payment_date":"2026-04-08"}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-time '{"client_id":1,"hours":2,"note":"..."}'
npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-expense '{"category":"software_saas","amount":99,"vendor":"..."}'
# send-invoice is disabled by policy — exits with code 2
```

Credentials loaded via `snappy-settings/load.ts` from `.env.cache`. Requires: `FRESHBOOKS_CLIENT_ID`, `FRESHBOOKS_CLIENT_SECRET`, `FRESHBOOKS_REFRESH_TOKEN`, `FRESHBOOKS_ACCOUNT_ID`.

## Key Capabilities

- **Create DRAFT invoices** -- `createInvoice()`. Always draft (`status: 1`). Never auto-sends.
- **Update DRAFT invoices** -- `updateInvoice()`. Refuses anything that isn't `v3_status === "draft"`.
- **Recurring retainer batch** -- 1st of month, draft all active retainers. Robert reviews and sends in bulk from FreshBooks UI. See `workflows.md#2`.
- **Time tracking** -- `createTimeEntry()`. Review unbilled hours on Fridays.
- **Expense logging** -- `createExpense()`. Always set category from: `software_saas`, `contractors`, `ads`, `tools_infra`, `professional`, `travel`, `office`.
- **Payment tracking** -- `markPaid()` records a payment against an invoice Robert already sent. This is bookkeeping, not sending.
- **Overdue follow-up** -- Day 7 email, Day 14 WhatsApp, Day 30 escalation. Always `dry_run: true` first. Robert sends the actual chase message.
- **Revenue dashboard** -- MRR, cash flow, profit margin, runway, churn detection.
- **Monthly close** -- Last Friday of month: revenue, expenses, profit, runway summary.

## FreshBooks API (direct, v3 accounting)

Base: `https://api.freshbooks.com`. Auth: OAuth2 refresh token (auto-handled by `api.ts`).

| operation | method | FreshBooks endpoint |
|---|---|---|
| List clients | GET | `/accounting/account/{id}/users/clients` |
| Create client | POST | `/accounting/account/{id}/users/clients` |
| List invoices | GET | `/accounting/account/{id}/invoices/invoices` |
| Create invoice | POST | `/accounting/account/{id}/invoices/invoices` |
| Update invoice | PUT | `/accounting/account/{id}/invoices/invoices/{inv_id}` |
| List time entries | GET | `/accounting/account/{id}/time_entries` |
| Create time entry | POST | `/accounting/account/{id}/time_entries` |
| List expenses | GET | `/accounting/account/{id}/expenses/expenses` |
| Create expense | POST | `/accounting/account/{id}/expenses/expenses` |
| Create payment | POST | `/accounting/account/{id}/payments/payments` |

## Common Mistakes to Avoid

- **Trying to send.** Don't. `sendInvoice` is a refusing stub and the `send-invoice` CLI exits with code 2. Draft and tell Robert; he sends from the FreshBooks UI.
- **Updating a non-draft invoice.** `updateInvoice` refuses anything whose `v3_status !== "draft"`. Once Robert has sent it, edits happen in the FreshBooks UI.
- **Assuming creation auto-emails.** It never has. Creation makes a draft with `status: 1`; that's the whole contract.
- `markPaid` creates a payment record on an already-sent invoice; it is bookkeeping, not sending.
- MRR = paid + outstanding recurring THIS month, not historical sent.
- Overdue chase starts Day 7, not Day 1. Always `dry_run: true` first.
- Use `getOrCreateClient` (idempotent) -- never create-only.

## Cadence Hooks (from snappy-ops)

| trigger | when | workflow |
|---|---|---|
| Daily EOD | every weekday | Log billable time |
| Friday close | every Friday | Outstanding invoices + unbilled time + overdue scan |
| 1st of month | first business day | Recurring retainer batch |
| Last Friday | last business day | Monthly close |

## Targets

Profit margin >= 60% (red < 50%). Receivables < 1mo expenses. Single client <= 30% MRR. Runway >= 6mo. Days to invoice <= 2.

## Files in This Skill

| file | purpose |
|---|---|
| `SKILL.md` | Full skill definition -- endpoints, decision map, cadence hooks, targets |
| `workflows.md` | 5 core workflows: new invoice, recurring batch, overdue follow-up, revenue dashboard, monthly close |
| `recurring-and-expenses.md` | Recurring retainers, expense entry, payment tracking, cash flow monitoring |
| `api-reference.md` | Full endpoint catalog with request/response shapes |

## Auth

OAuth2 with refresh token. `api.ts` handles token refresh automatically -- reads `FRESHBOOKS_CLIENT_ID`, `FRESHBOOKS_CLIENT_SECRET`, `FRESHBOOKS_REFRESH_TOKEN`, `FRESHBOOKS_ACCOUNT_ID` from `.env.cache` via `snappy-settings/load.ts`. Access token cached in memory with expiry tracking.

### Re-auth (when refresh token is empty or revoked)

Run the helper -- it handles the `authorization_code` flow, auto-discovers `FRESHBOOKS_ACCOUNT_ID` via `/auth/api/v1/users/me`, writes both back to `.env.cache`, then smoke-tests `listClients`:

```bash
# Pre-req: register a dev app at https://my.freshbooks.com/#/developer
#          redirect URI: https://api.freshbooks.com/auth/oauth/redirect
#          paste CLIENT_ID + CLIENT_SECRET into .env.cache (manual, one-time).
npx tsx ~/.claude/skills/snappy-freshbooks/scripts/reauth.ts
# -> prints auth URL, you authorize, paste ?code=XXX from redirect, done.

# Verify later without re-auth:
npx tsx ~/.claude/skills/snappy-freshbooks/scripts/reauth.ts --smoke-test
```

Idempotent. Does NOT touch CLIENT_ID/CLIENT_SECRET -- those are manual.

---

If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-freshbooks: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```


Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.

<!-- SKILL-INDEX-START -->
[snappy-freshbooks Index]|root: ~/.claude/skills/snappy-freshbooks|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,api-reference.md,recurring-and-expenses.md,workflows.md}
<!-- SKILL-INDEX-END -->

## Used by

- `snappy-slack`
- `snappy-telegram`
- `snappy-whatsapp`

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `clients` | — | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts clients` |
| `invoices` | — | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoices` |
| `invoice` | `invoice_id` | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoice <invoice_id>` |
| `list` | — | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts list` |
| `get` | `invoice_id` | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts get <invoice_id>` |
| `time-entries` | — | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts time-entries` |
| `expenses` | — | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts expenses` |
| `metrics` | `name` | `read` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts metrics "<name>"` |
| `create-invoice` | `payload` | `write` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts create-invoice <payload>` |
| `update-invoice` | `payload` | `write` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts update-invoice <payload>` |
| `send-invoice` | `invoice_id` | `send` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts send-invoice <invoice_id>` |
| `mark-paid` | `payload` | `pay` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts mark-paid <payload>` |
| `log-time` | `payload` | `write-reversible` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-time <payload>` |
| `log-expense` | `payload` | `write-reversible` | `npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-expense <payload>` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?