Version: 0.2.0 · Last updated: 2026-07-28
Zappier is a metered API platform: every API call your customers make
is priced per endpoint, adjusted by their customer type, tracked in a
usage ledger, and billed through Stripe once a day — or invoiced
manually by purchase order. Customers self-serve through the portal at
/portal. This manual covers running and operating the
system. For internals, see DEVELOPER.md; for
accounting procedures see ACCOUNTING.md;
for the end-user view see CUSTOMER-PORTAL.md.
cd /Users/marchon/zappier
npm install
npm run dev # starts the API on http://localhost:3000Environment variables (all optional except
STRIPE_SECRET_KEY for billing):
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
HTTP port for the API server |
ZAPPIER_DB |
<install root>/zappier.db |
SQLite database file location |
ADMIN_KEY |
admin-dev-key |
Key for the admin console and admin API |
ADMIN_USER |
admin |
Admin console primary username |
DEMO_ADMIN_USER / DEMO_ADMIN_PASSWORD |
demo / $$$Adm1n### |
Demo sign-in — override in production |
STRIPE_SECRET_KEY |
— (required for billing) | Billing job + portal reloads, loaded from .env |
All runtime paths (database default, .env, OpenAPI spec,
static assets) resolve from the installation root — the compiled server
(node dist/index.js) runs from any working directory, under
systemd, Docker, or cron.
The .env file at the repo root holds
STRIPE_SECRET_KEY. It is gitignored and owner-only
(chmod 600). A template is in
.env.example.
On first start the database is created and seeded with:
status (free),
storage-list (free), transform (fixed 4¢),
storage (variable: 10¢ base + 1¢/KB metadata + 50¢/MB
attachments)key-ada (Free),
key-grace (Pro), key-linus (Business)Seeding only happens into an empty database. Existing data is never overwritten on restart.
| Surface | URL / location | Who it’s for |
|---|---|---|
| Public API | http://localhost:3000/v1/* |
Your API customers |
| Interactive API docs | http://localhost:3000/docs |
Developers integrating with you |
| Admin console | http://localhost:3000/admin |
You (operations & accounting) |
| Customer portal | http://localhost:3021/portal/ (public door; loopback
edge still serves /portal on :3000) |
End-user customers (self-service) |
| Zapier app | zapier-app/ directory |
No-code users via Zapier |
Every priced call returns its quote in the response, so customers always know what a call cost:
{
"quote": {
"endpointId": "storage",
"listCents": 62,
"totalCents": 31,
"breakdown": { "baseCents": 10, "metadataCents": 2, "attachmentCents": 50 }
}
}The price of a call is computed in three steps:
free — always 0¢fixed — a flat fixedCents per callvariable — baseCents +
perKbCents × ceil(metadata bytes / 1024)
perMbCents × ceil(attachment bytes / 1 MB)totalCents = round(listCents × multiplier). A per-customer
multiplier override (set in the Customers tab) wins
over the type multiplier — use it for negotiated enterprise deals.Worked example. A Pro customer (×0.5) uploads a 1 MB
file with 2 KB of metadata to storage:
Open http://localhost:3000/admin and sign in. Two
accounts are available:
| Username | Password | Purpose |
|---|---|---|
admin |
the ADMIN_KEY env value (default
admin-dev-key) |
Primary operator |
demo |
$$$Adm1n### (env DEMO_ADMIN_PASSWORD) |
Demo / stakeholder access |
Sessions are token-based and remembered in browser local storage
until you click Sign out or the server restarts (tokens
are in-memory — just sign in again). The legacy x-admin-key
header still works for scripts and curl.
One row per API endpoint (matched by OpenAPI
operationId).
free /
fixed / variable) and the cent fields, then
click Save. Takes effect on the next API call — no
restart needed.operationId
(must match openapi.yaml), pick a kind, click
Add.
Types are your pricing tiers.
Stripe (metered by the daily
job) or Purchase order (manual invoicing with PO numbers;
see ACCOUNTING.md).PUT /admin/api/customers/:id with
{"stripeCustomerId": "cus_..."}) — see section 6.The Invoices tab generates monthly invoices from metered usage (per period, optionally per customer, with optional PO number), walks them draft → issued → paid, and opens print-ready invoice pages. The Reports tab produces date-ranged billing reports (all customers, one customer, or one billing type) with CSV download, plus daily/weekly usage-trend charts. The System tab shows Zapier integration health and the current-period billing snapshot. Full procedures: ACCOUNTING.md.
Your customers self-serve at
http://localhost:3021/portal/ (access-web; loopback edge
/portal remains on :3000): signup (or claiming
an account you created, by email), sign-in with optional TOTP two-factor
authentication, month-to-date usage, invoice history with print view,
prepaid balance reloads (drawn down automatically at invoice issue),
email-invoicing preferences, API-key regeneration, and live pricing. The
full end-user guide is CUSTOMER-PORTAL.md.
All calls need the customer’s API key in the x-api-key
header. Interactive docs with a “Try it out” console are at
/docs.
# Free status check
curl -H 'x-api-key: key-ada' http://localhost:3000/v1/status
# Fixed-price call (4¢ list)
curl -X POST -H 'x-api-key: key-grace' -H 'content-type: application/json' \
-d '{"text":"hello"}' http://localhost:3000/v1/transform
# Variable-price call: metadata + attachments
curl -X POST -H 'x-api-key: key-grace' \
-F 'metadata={"title":"Q3 report"}' \
-F 'attachments=@report.pdf' \
http://localhost:3000/v1/storage
# List your stored items (free)
curl -H 'x-api-key: key-grace' http://localhost:3000/v1/storage
# Your month-to-date usage, with credit applied
curl -H 'x-api-key: key-grace' http://localhost:3000/v1/usageLimits & validation: requests are validated against
openapi.yaml (bad requests get 400); attachments are capped
at 25 MB per file; metadata must be valid
JSON (400 otherwise).
A scheduled job runs the billing reporter daily at 06:17 America/New_York (Kimi cron job “Zappier billing · report usage to Stripe”). For each customer with a Stripe ID it:
zappier.api_cents, value = cents).billing_reports
ledger.Re-running is always safe: the ledger makes repeats no-ops, a per-run
lock prevents overlapping executions, and a deterministic Stripe
identifier (customer:period:billable) dedupes
crash retries.
Dashboard (test mode ON) → Billing → Meters → Create
meter: event name zappier.api_cents, aggregation
Sum of value, customer mapping
stripe_customer_id.
Product catalog → Add product “Zappier API usage” → price: recurring, monthly, metered against that meter, $0.01 per unit (1 unit = 1 cent).
For each billable customer: create the Stripe Customer, attach a payment method, and add a subscription with the metered price. Meter events for customers without a metered subscription are recorded but never invoiced.
Put the sk_test_... key into .env
(replace the placeholder).
Attach Stripe IDs to Zappier customers:
curl -X PUT -H 'x-admin-key: admin-dev-key' -H 'content-type: application/json' \
-d '{"stripeCustomerId":"cus_..."}' \
http://localhost:3000/admin/api/customers/cust_2npx ts-node src/jobs/report-usage.tsExpected output per customer:
skip <id> <period> (nothing to report) — no
billable usage yetskip <id> <period> (already reported Nc) —
no new usage since last run<id>: reported N billable cents to Stripe — delta
sentreport-usage: another run holds the lock, abort run —
safe concurrent abortThen check the meter’s Events tab in the Stripe dashboard and the test customer’s upcoming invoice.
Repeat 6.2 steps 1–3 in live mode, replace .env with the
sk_live_... key from the same Stripe
account, and keep the same cron. The live product
prod_Uxv9SAeIOZzyx1 (currently deactivated) can be
reactivated or recreated.
The zapier-app/ directory contains the Zapier Platform
app:
http://localhost:3000) and their
key-... customer key; the connection is tested against
/v1/status.GET /v1/storage for newly stored items.POST /v1/storage with metadata and optional file
attachments — billed per the rate card.To publish: create a Zapier developer account,
cd zapier-app && npm install && zapier login && zapier push,
then share the app or submit it to the Zapier marketplace.
| Task | How |
|---|---|
| Change a price | Admin console → Rate card → Save |
| Add a customer | Admin console → Customers → Create → copy the one-time API key |
| Set a customer’s billing type/email | Customers tab → Billing dropdown / Email field → Save |
| Invoice a period | Invoices tab → Generate → Issue → Mark paid (ACCOUNTING.md) |
| Export accounting data | Reports tab → filters → Download CSV |
| Give a customer a deal | Customers tab → multiplier override, or a new customer type |
| Turn an endpoint off | Rate card → Delete (calls get 403) |
| Check a customer’s usage | Their portal dashboard, GET /v1/usage with their key,
or query zappier.db |
| Check billing ran | Kimi notification after each 06:17 run; or run the job manually |
| Backup | Copy zappier.db (SQLite, single file) |
| Update dependencies | npm outdated, then npm test must stay
green (165 tests) |
| Symptom | Likely cause | Fix |
|---|---|---|
401 invalid or missing API key |
Wrong/absent x-api-key |
Re-issue key from Customers tab |
403 No price rule for ... |
Endpoint deleted from rate card and tier has no default rule | Re-add the rule, or set a tier defaultRule |
403 Unknown tier |
Customer’s tierId doesn’t exist |
Fix the customer’s type in the admin UI |
400 invalid metadata JSON |
metadata form field isn’t valid JSON |
Send e.g. {"key":"value"} |
413 ... file too large |
Attachment over 25 MB | Split or compress the file |
| Billing run: Stripe auth error | Placeholder/wrong key in .env, or key from a different
Stripe account |
Use the sk_test/sk_live key from the
account that holds the meter |
Stripe shows METER_NOT_FOUND invalid events |
Meter missing or event name mismatch | Meter event name must be exactly zappier.api_cents |
another run holds the lock, abort run |
Overlapping runs, or a crash left a stale lock | Safe by design; stale locks expire after 1 hour |
| Admin UI: “invalid username or password” | Wrong credentials, or env overrides changed them | Check ADMIN_KEY / DEMO_ADMIN_PASSWORD;
sign in again |
| Admin UI: “Session expired” | Server restarted (admin tokens are in-memory) | Sign in again |
Portal login: totp_required |
2FA is enabled on the account | Enter the current 6-digit authenticator code |
| Portal: “invalid or expired session” | 7-day session expired | Sign in again |
| Portal reload didn’t credit | Real STRIPE_SECRET_KEY configured → PaymentIntent
awaits confirmation |
Balance credits when the payment confirms; in dev (placeholder key) credit is instant |
Invoice went straight to paid on Issue |
Customer’s prepaid balance fully covered the amount due | Working as designed — balance was drawn down |