Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 133 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# zfy

[![CI](https://github.com/EssentialsDev/zfy-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/EssentialsDev/zfy-cli/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@devessentials/zfy-cli.svg)](https://www.npmjs.com/package/@devessentials/zfy-cli)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

A third-party CLI, TypeScript SDK, and **MCP server** for the [Zeffy API](https://www.zeffy.com/integration/api) — built so nonprofits can automate donation reporting and AI agents can answer questions like *"draft tax receipts for everyone who gave over $250 in 2025."*
Expand All @@ -18,51 +19,109 @@ Read-only by design — `zfy` cannot modify your Zeffy data, because the officia

## Contents

- [Install](#install)
- [Authenticate](#authenticate)
- [CLI usage](#cli-usage)
- [Quickstart](#quickstart)
- [Prerequisites](#prerequisites)
- [Quick start (5 minutes)](#quick-start-5-minutes)
- [CLI commands](#cli-commands)
- [End-of-year report](#end-of-year-report)
- [Logo spec for `--logo`](#logo-spec-for---logo)
- [MCP server](#mcp-server-use-from-claude--agents)
- [Optional: use from Claude or other AI agents (MCP)](#optional-use-from-claude-or-other-ai-agents-mcp)
- [SDK usage](#sdk-usage)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [License](#license)

## Install
## Prerequisites

You'll need two things before you start:

1. **Node.js 20 or newer.** Check with:
```bash
node --version
```
If that prints `command not found` or a version below `v20`, install the LTS from [nodejs.org](https://nodejs.org/) or via your package manager (`brew install node` on macOS).

2. **A Zeffy account.** You don't need a paid plan — Zeffy is free for nonprofits. The API key generator lives in your dashboard's **Settings → Integrations** section.

## Quick start (5 minutes)

### Step 1 — Install zfy

```bash
npm install -g @devessentials/zfy-cli
```

If `npm install -g` fails with a permissions error, you either need to fix your npm permissions or use [a Node version manager like nvm](https://github.com/nvm-sh/nvm). Don't run with `sudo` — it works but creates permission headaches later.

Verify it installed:

```bash
npm i -g @devessentials/zfy-cli
# or run without installing:
npx --package=@devessentials/zfy-cli zfy --help
zfy --version
# 0.1.0
Comment thread
EssentialsDev marked this conversation as resolved.
```

The package is `@devessentials/zfy-cli` on npm; the binary is `zfy` and the MCP entry point is `zfy-mcp`. Node.js 20+ required.
### Step 2 — Get your Zeffy API key

## Authenticate
1. Sign in at [zeffy.com](https://www.zeffy.com).
2. Open your organization's **Settings → Integrations** page.
3. Click **Generate API key**, copy the key (it starts with `sk_…`). You'll only see it once — store it somewhere safe.

1. In your Zeffy dashboard, go to **Settings → Integrations** and generate an API key.
2. Save it locally:
### Step 3 — Tell zfy your key

```bash
zfy auth set # prompts for the key, stores it in ~/.config/zfy/config.json (mode 0600)
zfy auth status # verifies the key by calling the Zeffy API
zfy auth clear # removes the stored key
zfy auth set
# pastes the prompt: paste your sk_… key, press Enter
Comment thread
EssentialsDev marked this conversation as resolved.
```

Or pass it inline for CI / one-offs:
This stores the key in your user config directory with restrictive permissions (mode 0600 — only you can read it). On most systems that's `~/.config/zfy/config.json`; if you set `XDG_CONFIG_HOME`, it lives under there instead. Run `zfy auth path` any time to see the exact location.

Verify the key works:

```bash
ZEFFY_API_KEY=sk_xxx zfy payments list --from 2025-01-01
zfy auth status
# Authenticated.
# Sample campaign: Annual Fund 2025
```

The `ZEFFY_API_KEY` env var always takes precedence over the stored key.
If you see that, you're done with setup.

### Step 4 — Run your first command

```bash
# Pull a single donation as a sanity check
zfy payments list --limit 1
```

You should see JSON like this (trimmed):

```json
[
{
"id": "pay_abc123",
"amount": 100,
"currency": "USD",
"status": "succeeded",
"type": "donation",
"contact": { "email": "donor@example.com", "name": "Jane Doe" },
"campaign": { "name": "Annual Fund 2025" },
"created": "2025-06-15T14:30:00Z"
}
]
```

That's it — every other command follows the same pattern. The output is always JSON so you can pipe it into [`jq`](https://jqlang.org/), an LLM, or your own scripts.
Comment thread
EssentialsDev marked this conversation as resolved.

### Step 5 — Generate your first end-of-year report

## CLI usage
```bash
zfy report eoy --year 2025 --format pdf --out ./receipts/ \
--org "Your Organization Name"
```

Every command outputs JSON to stdout — pipe it into `jq`, an LLM, a spreadsheet, or further commands.
This drops one PDF per donor in `./receipts/`. Open the directory and you'll see files like `2025-jane-doe.pdf`, each with the donor's annual total and itemized gifts — ready to mail or attach to an email.

### Quickstart
## CLI commands

Every command outputs JSON to stdout unless you use `--out` to write to a file.

Comment thread
EssentialsDev marked this conversation as resolved.
```bash
# Donations in a date range
Expand All @@ -81,9 +140,17 @@ zfy contacts list --email donor@example.com
zfy campaigns list
```

Pass `ZEFFY_API_KEY=sk_xxx` inline if you'd rather not store credentials — useful for CI:

```bash
ZEFFY_API_KEY=sk_xxx zfy payments list --from 2025-01-01
```

The environment variable always wins over the stored key.

### End-of-year report

Generate a per-donor annual report in any format. Defaults: excludes refunded payments, uses your system timezone for year boundaries.
Generates a per-donor annual report. Defaults: excludes refunded payments, uses your system timezone for year boundaries.

```bash
# JSON to stdout (default — best for piping to agents)
Expand All @@ -95,15 +162,13 @@ zfy report eoy --year 2025 --format csv --out eoy-2025.csv
# Top-50 donor markdown summary (fits in an LLM context)
zfy report eoy --year 2025 --format md --out eoy-2025.md --top 50

# One PDF receipt per donor
# One PDF receipt per donor (with optional logo)
zfy report eoy --year 2025 --format pdf --out ./receipts/ \
--org "Friends of the Library" \
--logo ./logo.png --logo-size 64 \
--timezone America/Los_Angeles
```

Useful flags (`zfy report eoy --help` for the full list):

| Flag | Notes |
| --- | --- |
| `--year <year>` | Required. Calendar year. |
Expand All @@ -115,12 +180,14 @@ Useful flags (`zfy report eoy --help` for the full list):
| `--top <n>` | Markdown: limit donor table. |
| `--org <name>` | PDF: organization name in the header. |
| `--logo <path>` | PDF: square PNG/JPEG mark — see spec below. |
| `--logo-size <pt>` | PDF: edge length of the logo slot (default 64 pt). |
| `--logo-size <pt>` | PDF: edge length of the logo slot in [PDF points](https://en.wikipedia.org/wiki/Point_(typography)) (default 64; 72 pt ≈ 1 inch). |
| `--receipt-text <txt>` | PDF: override the default tax-receipt boilerplate. |

Run `zfy report eoy --help` for the complete list.

### Logo spec for `--logo`

The PDF renderer reserves a small square slot for an org mark. To keep batches consistent and prevent multi-gigabyte receipt runs, logos must meet:
The PDF renderer reserves a small square slot for an org mark. Logos must meet these constraints — otherwise zfy prints a warning to stderr and falls back to text-only (no crash, batch keeps running):

| Constraint | Limit |
| --- | --- |
Expand All @@ -129,11 +196,13 @@ The PDF renderer reserves a small square slot for an org mark. To keep batches c
| Minimum dimensions | 64 × 64 px |
| Shape | Square within ±10% (e.g. 512×512, or 500×510 — but not 800×200) |

Recommended: a 512×512 PNG with a transparent or white background. If the file fails any check, `zfy` prints a single warning to stderr and continues without the logo — a bad asset never blocks a 500-donor receipt run.
**Recommended:** a 512×512 PNG with a transparent or white background.

## MCP server (use from Claude / agents)
## Optional: use from Claude or other AI agents (MCP)

`zfy-mcp` exposes the same operations as MCP tools over stdio. Add to `~/.claude.json` (or Claude Desktop's `claude_desktop_config.json`):
> Skip this section if you're only using the CLI directly. This is for plugging Zeffy into AI assistants that support the [Model Context Protocol](https://modelcontextprotocol.io/).

`zfy-mcp` is a stdio MCP server bundled with the package. Add it to `~/.claude.json` (or Claude Desktop's `claude_desktop_config.json`):

```json
{
Expand All @@ -155,7 +224,7 @@ Tools exposed:
| `zeffy_list_campaigns` | List campaigns |
| `zeffy_eoy_report` | Per-donor annual summary (JSON or Markdown) |

After installing and configuring, ask your agent things like:
Then ask your agent things like:

- "How much did we raise in Q4 2025?"
- "Who were our top 10 donors last year, and what did each give?"
Expand All @@ -165,7 +234,7 @@ PDF and CSV output aren't exposed via MCP (binary data is awkward over stdio)

## SDK usage

Everything the CLI does is available as a typed library, including the EOY aggregation and the format renderers.
Everything the CLI does is available as a typed TypeScript library, including the EOY aggregation and the format renderers.

```ts
import { Zeffy } from "@devessentials/zfy-cli";
Expand All @@ -192,18 +261,47 @@ console.log(formatMarkdown(report, 25));
await writePdfReceipts(report, "./receipts", { orgName: "Friends of the Library" });
```

The client handles 429 rate-limit responses (token-bucket capped at 90 req/min, with `Retry-After` honored on backoff) and validates every response against [zod](https://zod.dev) schemas — bad shapes throw with the raw response attached for debugging.
The client handles 429 rate-limit responses (token bucket capped at 90 req/min, with `Retry-After` honored on backoff) and validates every response against [zod](https://zod.dev) schemas — bad shapes throw with the raw response attached for debugging.
Comment thread
EssentialsDev marked this conversation as resolved.

## Troubleshooting

**`zsh: command not found: zfy`** (or `bash: zfy: command not found`)
Your shell can't find the binary that `npm install -g` just placed on disk. Run `npm prefix -g` to print your global install prefix (e.g. `/usr/local` or `~/.nvm/versions/node/v22.x.x`), then make sure `<prefix>/bin` is on your `PATH`. If you used `nvm`, switching Node versions changes the prefix — re-run the install after switching.

**`No Zeffy API key configured.`**
You haven't run `zfy auth set` yet, and `ZEFFY_API_KEY` isn't set in your environment. Run `zfy auth set` and paste the key when prompted.

**`API error 401: Invalid API key`** (or `Zeffy API error 401: ...` from other commands)
The key zfy has stored doesn't match what Zeffy expects. Either the key was regenerated (in which case run `zfy auth clear && zfy auth set` with the new one) or you copy/pasted with extra whitespace. `zfy auth status` is the simplest way to confirm — it hits the API and reports the failure.

**`Zeffy API error 429: Too Many Requests`**
You're hitting Zeffy's 100-requests-per-minute cap. zfy already throttles to 90/min and backs off on 429s, so this usually means something else on the same key is also calling the API. Wait a minute and retry.

**"Where did my PDFs go?"**
`zfy report eoy --format pdf` writes to the directory passed via `--out`, or — if `--out` is omitted — to `./eoy-<year>-receipts/` in your current working directory. The CLI prints the destination to stderr after it finishes.

**`zfy: skipping logo — image is not square (400×100); supply a square PNG/JPEG (e.g. 512×512)`**
Your `--logo` image violates the [logo spec](#logo-spec-for---logo). zfy continues without the logo so a bad asset doesn't block the rest of the report. Crop your image to a square in any image editor and rerun. Other variants of this message (file too large, too small, unsupported format) all skip the logo the same way.

**Want more detail on an error?**
Re-run with `DEBUG=1`:
```bash
DEBUG=1 zfy payments list --from 2025-01-01
```
This prints the full response body Zeffy returned, which usually pinpoints the issue.

## Development

```bash
git clone https://github.com/EssentialsDev/zfy-cli.git
cd zfy-cli
pnpm install
pnpm build # tsup → dist/
pnpm test # vitest
pnpm typecheck # tsc --noEmit
```

CI runs `typecheck`, `test`, and `build` on every push and PR against Node 20 and 22.
CI runs `typecheck`, `test`, and `build` on every push and PR against Node 20 and 22. Releases publish automatically to npm when a `v*` tag is pushed.

## License

Expand Down
Loading