{
  "schema": "snappy-card/1",
  "skill": "snappy-freshbooks",
  "tier": "public",
  "origin_tier": "base",
  "purpose": "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.",
  "verbs": [
    {
      "name": "clients",
      "args": [],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts clients"
    },
    {
      "name": "invoices",
      "args": [],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoices"
    },
    {
      "name": "invoice",
      "args": [
        "invoice_id"
      ],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts invoice <invoice_id>"
    },
    {
      "name": "list",
      "args": [],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts list"
    },
    {
      "name": "get",
      "args": [
        "invoice_id"
      ],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts get <invoice_id>"
    },
    {
      "name": "time-entries",
      "args": [],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts time-entries"
    },
    {
      "name": "expenses",
      "args": [],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts expenses"
    },
    {
      "name": "metrics",
      "args": [
        "name"
      ],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts metrics \"<name>\""
    },
    {
      "name": "create-invoice",
      "args": [
        "payload"
      ],
      "effect": "write",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts create-invoice <payload>"
    },
    {
      "name": "update-invoice",
      "args": [
        "payload"
      ],
      "effect": "write",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts update-invoice <payload>"
    },
    {
      "name": "send-invoice",
      "args": [
        "invoice_id"
      ],
      "effect": "send",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts send-invoice <invoice_id>"
    },
    {
      "name": "mark-paid",
      "args": [
        "payload"
      ],
      "effect": "pay",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts mark-paid <payload>"
    },
    {
      "name": "log-time",
      "args": [
        "payload"
      ],
      "effect": "write-reversible",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-time <payload>"
    },
    {
      "name": "log-expense",
      "args": [
        "payload"
      ],
      "effect": "write-reversible",
      "firstCall": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts log-expense <payload>"
    }
  ],
  "faces": [],
  "agents": [],
  "uses": [],
  "used_by": [
    "snappy-client-template",
    "snappy-slack",
    "snappy-telegram",
    "snappy-whatsapp"
  ],
  "checks": {
    "pass": 48,
    "fail": 6,
    "defer": 7,
    "line": "48 of 54 checks pass",
    "failing": [
      {
        "id": 4,
        "rule": "Near-neighbour descriptions cross-reference each other",
        "says": "near neighbours not named: snappy-client-feisst"
      },
      {
        "id": 10,
        "rule": "One term names each concept",
        "says": "mixed synonym sets: endpoint/url/route/path"
      },
      {
        "id": 40,
        "rule": "Metadata, skill body, and resources follow three-level budgets",
        "says": "metadata ~235 tokens; body ~4570 tokens"
      },
      {
        "id": 54,
        "rule": "At least three eval scenarios precede ready status",
        "says": "0 eval scenario(s); ready=true"
      },
      {
        "id": 55,
        "rule": "Eval scenarios are multi-call with ground truth",
        "says": "eval manifest missing"
      },
      {
        "id": 57,
        "rule": "Eval manifest contains a disjoint holdout",
        "says": "0 disjoint holdout scenario(s)"
      }
    ]
  },
  "coverage": null,
  "spec": null,
  "evals": null,
  "files": [
    "AGENTS.md",
    "SKILL.md",
    "api-reference.md",
    "api.ts",
    "entities.json",
    "face.test.ts",
    "faces/components/freshbooks-decision.tsx",
    "faces/components/freshbooks-invoice-list.css",
    "faces/components/freshbooks-invoice-list.tsx",
    "faces/components/freshbooks-invoice-preview.css",
    "faces/components/freshbooks-invoice-preview.tsx",
    "faces/family.tsx",
    "faces/fixtures/freshbooks-decision.json",
    "faces/fixtures/freshbooks-invoice.json",
    "faces/fixtures/freshbooks-list.json",
    "metrics.json",
    "read-limit.test.ts",
    "recurring-and-expenses.md",
    "refusals.test.ts",
    "scripts/reauth.ts",
    "stage.test.ts",
    "workflows.md"
  ],
  "total_size": 216576,
  "checksum": "sha256:be2802de84ef9c67456829549de9a57e1ee6b151e4d4ce897e17cd498664dfcf",
  "built_at": "2026-09-09T22:09:18.154Z",
  "openapi": {
    "openapi": "3.1.0",
    "info": {
      "title": "snappy-freshbooks",
      "version": "0.0.0",
      "description": "Snappy FreshBooks -- authoritative source for DRAFT invoicing, billing, time tracking, expense logging, recurring retainers, payment tracking, MRR / cash flow / profit dashboards, monthly close, overdue follow-up, and revenue analysis. Every Snappy skill that touches money routes through this one. This skill NEVER sends invoices -- it creates and updates drafts, and a human (Robert) sends from the FreshBooks UI after reviewing. Triggers on: invoice, freshbooks, billing, revenue, MRR, financial, payment, overdue, draft invoice, create invoice, update invoice, outstanding invoices, recurring invoice, retainer, monthly close, cash flow, expense, time entry, log time, billable hours, churn, client revenue, financial close, revenue dashboard, who owes me, profit margin, expense review, new client billing, onboarding billing, deal closed invoice, mark paid, payment received, late invoice, runway, monthly review, expense category."
    },
    "x-snappy-transport": "cli",
    "servers": [
      {
        "url": "npx tsx ~/.claude/skills/snappy-freshbooks/api.ts",
        "description": "The hand itself. Every path is a verb word, every query or body key an argument."
      }
    ],
    "paths": {
      "/clients": {
        "get": {
          "operationId": "snappy_freshbooks_clients",
          "summary": "snappy-freshbooks clients",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many clients to return. The count is the FLAG --limit, never a positional word; default 20, ceiling 100",
                "default": 20,
                "minimum": 1,
                "maximum": 100,
                "x-snappy-flag": "--limit"
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/invoices": {
        "get": {
          "operationId": "snappy_freshbooks_invoices",
          "summary": "snappy-freshbooks invoices",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many invoices to return, newest first. The count is the FLAG --limit, never a positional word; default 20, ceiling 100",
                "default": 20,
                "minimum": 1,
                "maximum": 100,
                "x-snappy-flag": "--limit"
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/invoice": {
        "get": {
          "operationId": "snappy_freshbooks_invoice",
          "summary": "snappy-freshbooks invoice",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "invoice_id",
              "in": "query",
              "required": true,
              "schema": {
                "type": "string",
                "description": "The FreshBooks invoice id, from an `invoices` row's `id`",
                "x-snappy-position": 0
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/list": {
        "get": {
          "operationId": "snappy_freshbooks_list",
          "summary": "snappy-freshbooks list",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many invoices to return, newest first. The count is the FLAG --limit, never a positional word; default 20, ceiling 100",
                "default": 20,
                "minimum": 1,
                "maximum": 100,
                "x-snappy-flag": "--limit"
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/get": {
        "get": {
          "operationId": "snappy_freshbooks_get",
          "summary": "snappy-freshbooks get",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "invoice_id",
              "in": "query",
              "required": true,
              "schema": {
                "type": "string",
                "description": "The FreshBooks invoice id, from an `invoices` row's `id`",
                "x-snappy-position": 0
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/time-entries": {
        "get": {
          "operationId": "snappy_freshbooks_time_entries",
          "summary": "snappy-freshbooks time-entries",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many time entries to return, newest first. The count is the FLAG --limit, never a positional word; default 20, ceiling 100",
                "default": 20,
                "minimum": 1,
                "maximum": 100,
                "x-snappy-flag": "--limit"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/expenses": {
        "get": {
          "operationId": "snappy_freshbooks_expenses",
          "summary": "snappy-freshbooks expenses",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many expenses to return, newest first. The count is the FLAG --limit, never a positional word; default 20, ceiling 100",
                "default": 20,
                "minimum": 1,
                "maximum": 100,
                "x-snappy-flag": "--limit"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/metrics": {
        "get": {
          "operationId": "snappy_freshbooks_metrics",
          "summary": "snappy-freshbooks metrics",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "parameters": [
            {
              "name": "name",
              "in": "query",
              "required": true,
              "schema": {
                "type": "string",
                "description": "Which counter to compute",
                "enum": [
                  "catchup-per-week",
                  "reconcile-per-week",
                  "catchup-apply-rate"
                ],
                "x-snappy-position": 0
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/create-invoice": {
        "post": {
          "operationId": "snappy_freshbooks_create_invoice",
          "summary": "snappy-freshbooks create-invoice",
          "x-snappy-effect": "write",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "string",
                      "description": "The invoice as a JSON object in FreshBooks' own invoice shape: customerid, create_date, lines",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "payload"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/update-invoice": {
        "post": {
          "operationId": "snappy_freshbooks_update_invoice",
          "summary": "snappy-freshbooks update-invoice",
          "x-snappy-effect": "write",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "string",
                      "description": "A JSON object carrying the invoice id and only the fields to change; the invoice must still be a draft",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "payload"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/send-invoice": {
        "post": {
          "operationId": "snappy_freshbooks_send_invoice",
          "summary": "snappy-freshbooks send-invoice",
          "x-snappy-effect": "send",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": true,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoice_id": {
                      "type": "string",
                      "description": "The FreshBooks invoice id, from an `invoices` row's `id`",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "invoice_id"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/mark-paid": {
        "post": {
          "operationId": "snappy_freshbooks_mark_paid",
          "summary": "snappy-freshbooks mark-paid",
          "x-snappy-effect": "pay",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": true,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "string",
                      "description": "A JSON object naming the invoice and the payment: invoiceid, amount, date",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "payload"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/log-time": {
        "post": {
          "operationId": "snappy_freshbooks_log_time",
          "summary": "snappy-freshbooks log-time",
          "x-snappy-effect": "write-reversible",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "string",
                      "description": "The time entry as a JSON object: client_id, project_id, duration in seconds, note",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "payload"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/log-expense": {
        "post": {
          "operationId": "snappy_freshbooks_log_expense",
          "summary": "snappy-freshbooks log-expense",
          "x-snappy-effect": "write-reversible",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "string",
                      "description": "The expense as a JSON object: categoryid, amount, date, vendor",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "payload"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      }
    },
    "components": {
      "schemas": {
        "SnappyRefusal": {
          "type": "object",
          "required": [
            "outcome",
            "code",
            "message"
          ],
          "properties": {
            "outcome": {
              "type": "string",
              "const": "refused"
            },
            "code": {
              "type": "string",
              "description": "A row of the closed refusal table."
            },
            "message": {
              "type": "string"
            },
            "fix": {
              "type": "string"
            }
          }
        }
      }
    },
    "x-snappy-requires": [
      "FRESHBOOKS_ACCOUNT_ID",
      "FRESHBOOKS_CLIENT_ID",
      "FRESHBOOKS_CLIENT_SECRET"
    ]
  }
}