PitchStation · Developers

The Invoice API: one secret, one call, one webhook

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.

Authentication

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.

Quickstart — the whole integration

# 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).
Dry-run safely: add "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.

Receipt mode — the money already moved

# 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.

Getting paid on the invoice — "Pay now"

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.

Multiple billing identities — issuers

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.

Compatibility & testing

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.

Correlation & bookkeeping

"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.

Webhooks — never poll

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));
}

The rest of the surface

CallDoes
POST /api/invoices/:id/paymentsRecord 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/pdfArchival 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/snapshotsThe sha256-indexed compliance PDFs frozen at each send.
POST /api/invoices/:id/credit-note · /void · /remindCorrections, voiding (number kept), manual reminder.
POST /api/invoices/:id/recurrenceMonthly retainers — drafted or auto-sent.
PATCH /api/invoices/settings/profileYour 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 Websites API — generate, approve, publish, measure

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>" }
  }'

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).

The fast paths (July 2026)

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.

The Apps API — host a running prototype, not just a document

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.

Dev Instances — a cloud workbench sized for one repository

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/provisioningPOST /: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.

Error catalog

HTTPCodeMeaning
401 / 403Token missing, revoked, or lacks the create.invoice capability.
400BAD_EXTERNAL_REFRef over 128 chars or outside A-Z a-z 0-9 . _ : / -.
400TEST_IMMUTABLE / REF_IMMUTABLEThe test flag and external_ref are fixed at creation.
409REF_PAYLOAD_MISMATCHSame external_ref, different amount/recipient — fix the caller, don't retry.
409NO_ITEMS / ZERO_TOTALsend:true needs at least one line item and a positive total.
409LOCKED_PAID / LOCKED_PARTIALPaid invoices are records — correct via credit note.
400PAID_REQUIRES_SEND / BAD_PAID_AMOUNT / BAD_METADATAReceipt mode needs send:true; paid amount within the total; metadata within its caps.
404BAD_ISSUER / COLLECT_OFFissuer_id not yours; Pay-now not enabled for this invoice.
409NO_GATEWAY_PAYMENT / NOT_PAYABLE / NOTHING_DUERefund needs a Stripe-settled payment; checkout needs a sent invoice with a balance.
400CURRENCY_UNSUPPORTED_FOR_COLLECTZero-decimal currency amount not a whole unit — fix the cents.
429SEND_CAPDaily send cap for your plan (20 free / 200 pro / 1000 team). Deliberate: it bounds what a leaked token can email. Contact support to raise.
503PDF_RENDER_OFF / PDF_BUSYRenderer disabled or briefly saturated — retry later or skip the PDF.

Rules worth knowing

Integer centsNever send floats. unit_cents: 250000 is $2,500.00. Totals are recomputed server-side from items + tax − discount.
Delivery gatesA 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.
NumberingGap-free per account, assigned at send (INV-2026-0042, prefix configurable). Drafts are unnumbered; voids keep their number.
RemindersAutomatic at due−3 / due / +7 / +14, stopping on paid. Per-invoice opt-out.
Rate limitsSend caps above; webhook endpoints max 3 per account. Standard API limits apply to bursts.
PitchStation · Invoice & Websites APIs · spec version 2026-07 · signed-in API reference with all products: /api-docs.html · questions: hello@pitchstation.ai