Create an invoice from your platform and PitchStation does the rest — tracked delivery with the password logic handled, automatic reminders before and after the due date, client "I've paid" claims, receipts, credit notes, archival PDFs, and signed webhooks back to you. Money is integer cents, everywhere, always.
Every call carries a Personal Access Token: Authorization: Bearer pst_…. Sign in and mint one at /tokens.html — scope it to create.invoice so a leaked token can touch nothing else. Tokens show their last-used time there; rotate freely.
# One idempotent call: create + number + email + start chasing.
curl -X POST https://www.pitchstation.ai/api/invoices \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d '{
"external_ref": "order-2026-00417",
"send": true,
"client_name": "Acme Corp", "client_email": "ap@acme.com",
"currency": "USD", "due_date": "2026-08-15", "terms": "NET 30",
"items": [{ "description": "Vending machine x3", "qty": 3, "unit_cents": 250000 }]
}'
# → 201 { "number": "INV-2026-0031", "url": "https://…/s/…?k=…",
# "password": "amber-falcon-42", ← only for clients without an account
# "invoice": { "id": 87, "status": "sent", … } }
# Retried the call? Same external_ref + same payload → 200, idempotent_replay: true.
# Same ref but a DIFFERENT amount/recipient → 409 REF_PAYLOAD_MISMATCH (that's a bug, not a retry).
"test": true — the invoice numbers in a separate TEST- series, no email leaves (the response shows would_email), the artifact is stamped TEST INVOICE, it never touches your aging or our metrics, and it self-purges after 30 days.# Your PSP collected; you need the numbered document. One call:
curl -X POST https://www.pitchstation.ai/api/invoices \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d '{ "external_ref": "order-417", "send": true,
"paid": { "method": "stripe", "reference": "pi_3PqK…" },
"client_name": "Acme", "client_email": "ap@acme.com",
"currency": "USD", "items": [{ "description": "Order 417", "qty": 1, "unit_cents": 87825 }] }'
# → 201 { number, url, invoice: { status: "paid" } } — PAID stamp, RECEIPT email, no reminders.
# Provenance note: caller-attested payments record as declared_by "owner";
# "gateway" is reserved for settlements we verified by signature ourselves.
When the server has collection enabled and you've opted your account in (POST /api/invoices/settings/rails {"rail":"stripe","enabled":true}), every payable invoice page shows a Pay now button: the client pays by card on a Stripe-hosted page and the invoice flips to PAID by signed webhook — no human touch, exactly-once, amount-verified. Your invoice.paid webhook fires as usual. Refunds ride credit notes: POST /api/invoices/:id/credit-note {"refund": true} refunds the Stripe payment and records the correction (event invoice.refunded). FLAG requires server-side collection to be enabled — ask before relying on it.
One account can invoice under several identities (entities, brands, stores): GET/POST/PATCH/DELETE /api/invoices/issuers — each issuer carries its own legal block, logo, and its own gap-free number register ({"number_prefix":"HK"} → HK-2026-0001). Pass "issuer_id" on create; filter lists with ?issuer_id=; webhook payloads carry it. Your existing single-profile setup is the "default issuer" — nothing changes until you add a second.
"metadata": { … } (≤20 keys, ≤2 KB, scalar values) is stored and echoed verbatim in every webhook and GET — never rendered on the document. List filters: GET /api/invoices?status=…&since=YYYY-MM-DD&client_email=…&limit=…&offset=… (returns total). Month-end rollup: GET /api/invoices/reconciliation?month=YYYY-MM — issued / paid-by-gateway / paid-manual / refunded by currency and issuer, plus current outstanding/overdue.
curl -X POST https://www.pitchstation.ai/api/webhooks \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d '{ "url": "https://your-platform.com/hooks/pitchstation",
"events": ["invoice.viewed", "invoice.paid", "invoice.overdue"] }'
# → 201 { "endpoint": { "id": 3, … }, "secret": "whsec_…" } ← shown ONCE, store it
# Every delivery is signed:
# X-PitchStation-Signature: t=1789300000,v1=<hex>
# v1 = HMAC-SHA256(secret, t + "." + rawBody) — reject if |now−t| > 300s
# Retries: 1m / 10m / 1h / 6h / 24h, then dead-letter + email to you.
# POST /api/webhooks/:id/test sends a signed ping; GET /api/webhooks/:id/deliveries is the log.
Events: invoice.sent · invoice.viewed · invoice.payment_claimed · invoice.paid · invoice.overdue · invoice.voided · invoice.credit_noted. Payloads carry ids, external_ref, status, and totals — never document content.
// Verify in Node:
const crypto = require('crypto');
function verify(sigHeader, rawBody, secret) {
const { t, v1 } = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expect = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expect), Buffer.from(v1));
}
| Call | Does |
|---|---|
POST /api/invoices/:id/payments | Record money received (blank amount = full balance) — e.g. when your PSP confirms. Full payment → PAID stamp + receipt email. |
GET /api/invoices?external_ref=… | Look an invoice up by your order id. |
GET /api/invoices/:id/pdf | Archival application/pdf (drafts carry a DRAFT stamp). FLAG requires the server's PDF renderer to be enabled — expect PDF_RENDER_OFF until it is. |
GET /api/invoices/:id/snapshots | The sha256-indexed compliance PDFs frozen at each send. |
POST /api/invoices/:id/credit-note · /void · /remind | Corrections, voiding (number kept), manual reminder. |
POST /api/invoices/:id/recurrence | Monthly retainers — drafted or auto-sent. |
PATCH /api/invoices/settings/profile | Your seller identity on every artifact: legal name, tax ID, address, footer, logo (PNG/JPEG/WebP only — transcoded server-side). Frozen onto each invoice at send. |
Full OpenAPI 3 spec: /api/openapi-invoices.json — import it into Postman or generate a client.
The same PAT drives programmatic website creation — built for platforms whose users each need a personal site (agents, brokers, franchisees). One call creates the site from your content JSON; the built-in approval loop means nothing goes live without a compliance sign-off on an audited link; one more call publishes it to a real server with its own domain and TLS; aggregate visitor metrics report back.
curl -X POST https://www.pitchstation.ai/api/website/generate \
-H "Authorization: Bearer $PITCHSTATION_PAT" -H "Content-Type: application/json" \
-d '{
"external_ref": "agent-12345",
"metadata": { "store": "bridgelink360", "agentLevel": "L2" },
"content": {
"meta": { "company_name": "Alice Chen — Bridgelink 360", "lang": "en" },
"hero": { "lede": "Your neighborhood tech partner.", "chips": ["Phones", "Tablets", "Smart home"] },
"contact": { "email": "alice@example.com", "website_url": "https://shop.example.com/?ref=12345" }
},
"assets": { "logo": "<base64>" }
}'
external_ref returns the existing site (replayed: true) — sites are mutable, use /update to change them.website.generated · website.submitted · website.approved · website.changes_requested · website.published · website.unpublished — your external_ref and metadata echo on every event./submit-review (your compliance operator approves on the tracked link) → PUT /domain (subdomain or full custom domain) → /publish (dry-run plan first; live publishes to one server queue FIFO) → /metrics for visits · /unpublish on churn (releases kept, re-publish is instant).GET /templates) — partner accounts can be template-locked server-side, so brand drift is impossible by construction.Full OpenAPI 3 spec: /api/openapi-websites.json. Website-specific error codes: BAD_EXTERNAL_REF, BAD_METADATA, SITE_QUOTA (429, plan site cap), PUBLISH_QUOTA (429, daily live publishes), NOT_APPROVED (409, approval gate), SUBDOMAIN_TAKEN (409).
| HTTP | Code | Meaning |
|---|---|---|
| 401 / 403 | — | Token missing, revoked, or lacks the create.invoice capability. |
| 400 | BAD_EXTERNAL_REF | Ref over 128 chars or outside A-Z a-z 0-9 . _ : / -. |
| 400 | TEST_IMMUTABLE / REF_IMMUTABLE | The test flag and external_ref are fixed at creation. |
| 409 | REF_PAYLOAD_MISMATCH | Same external_ref, different amount/recipient — fix the caller, don't retry. |
| 409 | NO_ITEMS / ZERO_TOTAL | send:true needs at least one line item and a positive total. |
| 409 | LOCKED_PAID / LOCKED_PARTIAL | Paid invoices are records — correct via credit note. |
| 400 | PAID_REQUIRES_SEND / BAD_PAID_AMOUNT / BAD_METADATA | Receipt mode needs send:true; paid amount within the total; metadata within its caps. |
| 404 | BAD_ISSUER / COLLECT_OFF | issuer_id not yours; Pay-now not enabled for this invoice. |
| 409 | NO_GATEWAY_PAYMENT / NOT_PAYABLE / NOTHING_DUE | Refund needs a Stripe-settled payment; checkout needs a sent invoice with a balance. |
| 400 | CURRENCY_UNSUPPORTED_FOR_COLLECT | Zero-decimal currency amount not a whole unit — fix the cents. |
| 429 | SEND_CAP | Daily send cap for your plan (20 free / 200 pro / 1000 team). Deliberate: it bounds what a leaked token can email. Contact support to raise. |
| 503 | PDF_RENDER_OFF / PDF_BUSY | Renderer disabled or briefly saturated — retry later or skip the PDF. |
| Integer cents | Never send floats. unit_cents: 250000 is $2,500.00. Totals are recomputed server-side from items + tax − discount. |
| Delivery gates | A client email matching a PitchStation account gets an account-locked link (no password); anyone else gets link + password in the same email, rotating on resend. |
| Numbering | Gap-free per account, assigned at send (INV-2026-0042, prefix configurable). Drafts are unnumbered; voids keep their number. |
| Reminders | Automatic at due−3 / due / +7 / +14, stopping on paid. Per-invoice opt-out. |
| Rate limits | Send caps above; webhook endpoints max 3 per account. Standard API limits apply to bursts. |