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.
The API evolves additively — new fields and endpoints appear; existing shapes, codes and semantics do not change or disappear. No version prefix to pin: write tolerant readers (ignore unknown fields) and you will never break. Use "test": true for anything exploratory, and poll GET /api/invoices/reconciliation?month=YYYY-MM at month-end for your books. AI drafting endpoints (/chat, /draft, /extract-contract) carry a daily per-user allowance — 429 INVOICE_AI_QUOTA; composing JSON directly is never capped.
"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"] },
"products": [{ "image": "<data URI>", "name": "Galaxy S26", "price": "US$899", "link": "https://shop.example.com/p/s26?ref=12345" }],
"testimonials": [{ "quote": "Fast, honest, always available.", "name": "M. Rivera", "role": "Customer since 2024" }],
"video": { "url": "https://www.youtube.com/watch?v=XXXXXXXXXXX" },
"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.meta.layout picks the page structure (classic · editorial · showcase · monolith · atelier · sidenav · narrative) and composes with every template; meta.section_order reorders sections (hero/contact pinned); per-section variants via services_layout/products_layout/team_layout. Layouts are never locked — the template lock pins brand, structure stays yours.feature_rows[] (alternating feature sections), product specs[] with an auto-derived comparison table, badges[] trust strip, contact.chat (WhatsApp/Telegram/LINE/WeChat floating button + per-product ask links), price_was, options[] variant chips, collection grouping, announcement bar, image_hover. Fashion wave: contact.social[] visible icon rows, size_guide measurement table, product tag ribbons, shoppable gallery[].links, announcement.countdown, marquee ticker. Vertical starters in the catalog: presets[] (electronics, jewelry, fashion) plus retail templates volt, maison and runway.products[], testimonials[], gallery[] (lightbox with zoom), pricing[] (tiers), clients[] (logo wall), cta_banner, video (YouTube/Vimeo, rendered as a click-to-load facade — nothing third-party loads before the visitor clicks play). Site chrome languages: zh-Hant · zh-Hans · en · es.Full OpenAPI 3 spec: /api/openapi-websites.json. A complete integration guide (lifecycle, content model, catalog, onboarding checklist) is available to partners on request — ask hello@pitchstation.ai. Website-specific error codes: BAD_EXTERNAL_REF, BAD_METADATA, BAD_VIDEO_URL (400, video must be YouTube/Vimeo), BAD_SECTION_ORDER (400, unknown/duplicate section ids), BAD_PAGES (400, invalid multi-page map), BAD_PRESET / BAD_SOURCE / BRIEF_TOO_SHORT / BAD_LANG (400), BAD_TOKENS (400, brand tokens outside bounds), BAD_DOMAIN / DOMAIN_TAKEN (400/409, base-domain claims), ASSIST_QUOTA (429, per-day AI pool), SITE_QUOTA (429, plan site cap), PUBLISH_QUOTA (429, daily live publishes), NOT_APPROVED (409, approval gate), SUBDOMAIN_TAKEN (409), LLM_NOT_CONFIGURED (503, brief drafting needs server AI).
No content model? Send a brief. POST /api/website/generate {"brief":"a plain-text company description (40–24,000 chars)","lang":"en","assets":{"logo":…}} — the server AI-drafts the model and renders in one call, echoing the draft + gap warnings. Start from a preset: "preset":"agent"|"electronics"|"jewelry"|"fashion" expands server-side under your values (the agent preset is the partner referral card — mount each agent's unique link in cta_banner.link; identical content across an account raises a DUPLICATE_CONTENT warning). Multi-page: content.meta.pages=[{"slug":"","title":"Home","sections":[…]},…] (≤8 pages, slug "" = index) renders one static file per page with shared nav; review links cover the whole site. Draft without creating: POST /api/website/extract accepts a brochure PDF, up to 6 photos, one .txt/.md file, or JSON {"text":…} — one response contract for all four.
Partner self-service. POST /api/website/base-domain {"domain":"sites.yourbrand.com"} → add the returned TXT + wildcard-A records → /base-domain/verify: every site then publishes under YOUR domain. PUT /api/website/brand-tokens {"accent":"#FF6800","ink":"#001F5B","paper":"#FFFFFF","font_pair":"montserrat"} re-skins your (locked) template within accessibility clamps — a rebrand is one call.
Websites are static; some deliverables are running software. The Apps API deploys any project with a
docker-compose.yml to a VPS you registered and returns a live TLS URL at <slug>.apps.pitchstation.ai
(or your own domain) — optionally behind a basic-auth preview gate so an unfinished prototype is never one forwarded URL away from public.
PitchStation never runs your code: the control plane orchestrates your host over SSH, holds the credential encrypted, and keeps the release history.
Managed publishing pilot · 18 September 2026. The platform is deployed and the existing Apps target is enabled with six inventoried apps. Their content, passwords and expiry are unchanged. Custom-host TXT ownership proof and first managed route adoption remain pending; this is not availability for every target or account. Discover GET /api/apps/publishing-capabilities and the owned app’s /domain-config; do not infer readiness from a feature flag.
Content and configuration are separate. Create an app once, inspect a deploy dry-run, then upload reviewed content through /deploy. For an existing app’s addresses or access, use the Go-live workspace, PUT /:id/domain-config or PATCH /:id/publication-policy: plan → verify each hostname → apply → check readiness. These configuration operations do not build, create a release or restart the app. They require If-Match, the reviewed plan ID and Idempotency-Key on apply. Correct A/CNAME routing can stay; add the app-scoped TXT proof at your DNS provider.
Durable work, explicit limits. Managed deploy/rollback with Prefer: respond-async returns 202 and an operation ID; reconnect by polling or retrying identical intent with the same key. A container or shared database can change before routing succeeds; code rollback does not undo migrations. Env PATCH preserves unrelated keys and restarts the current release; env PUT remains replace-all/stored-only. Private policy requires a retained password; platform public access cannot remove the application’s own login/expiry. HTTP readiness is not an interactive browser test.
Agent entry points: create-pitchstation-app skill, the repository client node scripts/pitchstation-apps.cjs help (run from generator/ with dependencies), or REST. No Apps MCP tools are currently exposed. Downloading the skill does not install the client or grant deployment rights. Setup and examples · API workflow.
Certificate staging checks passed for all ten existing hostnames. Production renewal acceptance and separate alert delivery remain pending. APP_ACME_EMAIL is not an alert subscription: Let’s Encrypt ended expiry emails; PitchStation monitoring/email were not enabled with this pilot.
Full OpenAPI 3 spec: /api/openapi-apps.json. Access is gated on the
app.deploy capability, which is never granted by default — running your own prototypes and running a stranger's code are different
products, so this one arrives only by an explicit admin grant.
A dev instance is a VPS with Claude Code, Kimi CLI or Codex pre-installed, sized from the repository it will hold.
PitchStation is the registry and policy engine only: it never makes a cloud call and holds no login credential for any
box. Your machine provisions (AWS, under a scoped IAM profile) and confirms each step; the box pushes its own telemetry with a
per-box report token and stops itself when idle. The protocol is POST /api/devboxes (reserve — capability,
name and your monthly cost cap are checked before anything exists in the cloud) → the provisioner launches from
GET /:id/bootstrap (secret-free, pinned cloud-init) → POST /:id/provisioning → POST /:id/token
(rotate-on-call, shown once) → POST /:id/finalize. Lifecycle confirmations (/started, /stopped,
/resizing, /resized, /terminated) are compare-and-set on the row version and replay-safe with an
Idempotency-Key. DELETE tombstones and is refused while an instance is attached.
Sizing is one pure function: POST /api/devboxes/size takes the profile from scan.py and answers the class,
the all-in monthly price (compute + storage + public IPv4, running and stopped) and the reasons. The agent-facing surface is the
pitchstation-devbox skill with its devbox.py CLI;
code moves with git (the box is remote devbox; never rsync --delete, never force). Your dev instances and
their cost to date: /devboxes.html. Full spec: /api/openapi-devboxes.json.
Gated on devbox.manage, explicit grant only; team members can read boxes shared with their team.
| 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. |