Curb · plate-as-credential SaaS
Plate-as-credential SaaS for independent parking operators, mid-tier EV charging networks, multi-family properties, and fleet depots. Self-serve onboarding, Stripe-style developer experience, OCPP 2.0.1 out of the box. Recently pulled forward: the M2 + M3 capability horizon — network hotlist, cross-operator roaming, audit-defensible dynamic pricing, and an ISO 15118 Plug-and-Charge bridge — all in code, with 212 tests passing in 4.2 s.
01 — Who it's for
The enterprise tier (Metropolis, Flash, Indigo, LAZ) is well-served. The long tail — sub-500-stall operators, mid-tier EV networks, residential garages, fleet depots — cannot justify those contracts but desperately needs to retire its cash kiosks. That's the Curb market.
— ICP · 01 · Independent parking
12 surface lots, 800 stalls, southwest US. Spends $40K/yr maintaining $25K kiosks with 8% cash leakage. Wants to retire both and redirect the budget to a per-lane subscription.
— ICP · 02 · EV charging
60 chargers across 14 sites. Currently pays per-charger SaaS to a competitor. Wants Autocharge-class UX without locking drivers into one OEM ecosystem — OCPP, fewer apps.
— ICP · 03 · Multi-family
480-unit rental, 60 EV chargers, 240 reserved stalls. Wants residents to park and charge with no decals, no cards, no app — plus sub-metering and guest enforcement.
— ICP · 04 · Fleet ops
4 depots, 200 trucks each, mixed diesel and electric. Wants automated yard gate-in / gate-out for 200 drivers per shift, per-vehicle charging attribution, and Samsara sync.
02 — How it works
The Curb edge agent runs at each lane or charger and posts detects to the control plane. The state machine opens a session on entry, accumulates charging activity if it's an EV lane, closes on exit, prices against the rate-card snapshot, and routes the invoice through Stripe Connect.
For any lane
Edge agent reads the plate, posts to /v1/sessions or runs in-process through the SessionEngine. Plate, camera, timestamp, confidence — one POST per arrival.
For noisy OCR environments
Exact then Damerau-Levenshtein fuzzy ≤ 1 edit. Ambiguous matches refuse to auto-bill — they queue for operator review rather than guessing.
For dispute-defensible billing
OPEN → IN_PROGRESS, with the rate-card snapshotted at open time. Future edits don't change closed sessions. That's what makes Curb's pricing defensible in a chargeback dispute.
For hourly, daily-cap, peak, EV
Exit detect closes the session. Pricing engine applies hourly, daily cap, grace, rounding, peak windows, EV kWh, and idle fees — one JSON in, one breakdown out.
For operators using Stripe Connect
Stripe Connect Custom account; idempotent re-invoice. Zero-amount sessions short-circuit and flip to INVOICED without hitting Stripe. Payouts to the operator's bank, not ours.
For chargebacks & goodwill refunds
72-hour window from session.ended_at. Resolve uphold (charge stands) or refund (session → VOID, invoice → REFUNDED). Every step is in the audit log.
03 — Pricing engine
A rate card is a JSON document. It snapshots into the session at open time — so editing the card
tomorrow never retroactively changes a closed session. The breakdown returned by
calculate_price() surfaces every line item: parking_cents, ev_energy_cents,
ev_idle_cents, rounded_minutes, peak_multiplier, daily_cap_applied, grace_applied.
# Rate card for a downtown parking garage with peak-hour pricing # and a daily cap for monthly parkers. { "currency": "USD", "hourly_cents": 500, # $5.00 per hour "daily_cap_cents": 2500, # never exceed $25/day "minimum_cents": 200, # $2 minimum charge "grace_minutes": 5, # first 5 min free "rounding_minutes": 15, # round up to nearest 15 "peak": { "windows": [ {"days": [0,1,2,3,4], "start": "07:00", "end": "10:00", "multiplier": 1.5} # weekday rush hour ] }, "ev_kwh_cents": 35, # $0.35/kWh dispensed "ev_idle_per_min_cents": 25 # $0.25/min occupancy when full }
→ operator (Priya Parking · plan_tier=lane) — api_key → site (Lot A, address, tz) → zone → lane (kind=entry|exit|combo|ev) → camera + charger ·
Every row carries an operator_id; tenant isolation is enforced at every endpoint.
04 — EV charging
Curb speaks OCPP 2.0.1 (and 1.6 for legacy chargers) directly to the charge-point. Authorize requests resolve against the operator's plate_account table — hotlisted plates are blocked at the gateway regardless of account status. TransactionEvents auto-link to the parking session on the same lane, so the bill is one line item, not two.
For any compliant charger
Hotlist trumps account status. Plate-as-idToken means the driver doesn't pull out a card or open an app — the car authenticates itself.
For auto-linked parking + charging
Opens an ocpp_session row, links to the open parking session on the same lane (if any), records meter_start_kwh. One arrival event in the operator log, not two.
For live dashboard kWh
Real-time meter_stop_kwh updates so dashboards see the kWh dispensed as it happens. Drivers can watch the energy delivered through the operator's own portal.
For final billing reconciliation
kwh_used = stop − start. Adds charger.session.started / .stopped events to the parking session log. The receipt shows the actual energy delivered, not the requested.
Compatible chargers tested at GA (M3): ABB Terra, Wallbox Pulsar, ChargePoint Express, Tesla Wall Connector (Gen 3 with OCPP firmware), Enphase IQ, Schneider EVlink. Adding a new model is a vendor-adapter PR — the gateway interface is stable from 1.0.
05 — Public REST API
All endpoints under /v1, X-Curb-Api-Key on every request, tenant isolation enforced at
every handler. The OpenAPI 3.1 spec is generated by FastAPI at /docs — same surface
human-browsable and machine-consumable.
# 1. Mint a session by enrolling 50 employee plates in one POST. curl -X POST https://api.curb.dev/v1/plates/bulk \ -H "X-Curb-Api-Key: sk_live_…" \ -H "Content-Type: application/json" \ -d '{"csv_text":"plate,email,tag\nEMP1001,a@co,employee\nEMP1002,b@co,employee\n…"}' # Response: 200 { "enrolled": 50, "skipped": 0, "errors": [] } # 2. Real-time plate lookup with fuzzy fall-back. curl "https://api.curb.dev/v1/plates/lookup?plate=abc1z3" \ -H "X-Curb-Api-Key: sk_live_…" { "confidence": "fuzzy", "account": { "id": 42, "primary_plate": "ABC123", "email": "driver@example.com", "billing_status": "active" }, "candidates": [42] } # 3. List today's disputed sessions for triage. curl "https://api.curb.dev/v1/sessions?status=disputed" \ -H "X-Curb-Api-Key: sk_live_…"
06 — Webhooks
Curb signs every webhook with HMAC-SHA256 in the same t=<ts>,v1=<hex> header
format Stripe uses. If your team already verifies Stripe-Signature, verifying Curb-Signature is five lines.
# Inside your webhook handler: from curb.webhooks import verify_signature def webhook_handler(request): body = request.body.decode("utf-8") sig = request.headers["Curb-Signature"] if not verify_signature(WEBHOOK_SECRET, body, sig, max_age_seconds=300): return Response(status=401) payload = json.loads(body) # … process payload.session_id, payload.amount_cents, … return Response(status=200)
07 — Operator console + driver mobile-web
The operator console is server-rendered FastAPI + HTMX — no SPA build, no Node toolchain, runs anywhere Python runs. The driver-facing mobile-web is a single touch-friendly form that completes enrollment in under 45 seconds.
Plate accounts, hotlisted plates, recent sessions table, plan tier banner. Everything an operator needs in the first 10 seconds after login.
Filter by status (open / in_progress / closed / invoiced / disputed / void). Click into per-session event log with full payload pretty-print.
Single-row form to add a plate account. Bulk CSV import via the REST API at /v1/plates/bulk. Both surfaces share the same idempotency rules.
Add a plate to the operator-scoped hotlist with reason. Optional expiry — useful for "30-day banned" rules without a manual cleanup task.
Site / zone / lane hierarchy with one-click QR-mint for driver enrollment at any lane. The QR is a one-shot token; the URL is short enough to print on a sign.
Mobile-web form behind a one-shot token. Plate + optional email. "You're set" confirmation with a dashed-border plate display. No app install, no account creation friction.
08 — Capability horizon · M2 / M3 / Post-1.0, pulled forward
PRD-02's M2 + M3 + Post-1.0 capability features are now in code: a consent-gated network hotlist, reciprocal cross-operator plate roaming with revenue-share, audit-defensible dynamic pricing, and an ISO 15118 Plug-and-Charge bridge over the existing OCPP layer. Real Protocol architecture + deterministic stub adapters; production integrations plug behind the same Protocols.
vs covert cross-operator hotlist exchange
Closed HotlistReason enum (chronic_non_payment, incident_damage, incident_theft, incident_harassment, expired_due_recovery, operator_request) — no free-text reasons. Plate values are SHA-256 hashed; cross-operator adoption requires the originating operator to opt the flag into the network and the adopting operator to explicitly affirm. check_plate() returns reason-keyed action — every match is audit-chained.
vs per-operator lock-in (TollPass, ParkMobile, ChargePoint)
RoamingAgreement between two operators (signed + counter-signed, explicit revenue-share %). RoamingSession lifecycle: opened → roaming_auth_requested → authorised → closed → settlement_pending → settled. find_agreement() + compute_split() helpers. Driver consent for roaming is a separate enrolment flag from the basic Curb signup.
vs opaque "surge" multipliers
DynamicPricingEngine composes time-of-day rules, occupancy surges / discounts, and event-window overrides. Closed AdjustmentReason enum so every price line on the receipt is audit-defensible (no opaque "surge" numbers). Hard ceiling + floor multipliers enforce consumer-protection bounds.
vs Autocharge MAC-spoofing (ineligible for many CSMS)
ContractResolver Protocol + StubContractResolver for cert → customer lookup. to_ocpp_authorize() translates a successful PnC handshake into an OCPP 1.6 / 2.0.1 Authorize.req payload (idTokenType eMAID with MO-Contract + Cert-SHA256 additional info) so the existing CSMS layer (curb.ocpp) doesn't need PnC awareness. Certificate body is not retained — only the fingerprint hash.
+55 new tests across test_hotlist.py, test_roaming.py, test_dynamic_pricing.py, test_iso15118.py. PRD-02 §9 M2 + M3 + Post-1.0 capability surface now in code. Remaining milestones are commercial: PCI DSS L1, SOC 2 Type I / II, 200 / 1000 paying-operator targets, 6-charger-model certification matrix.
09 — Pricing
No enterprise sales. No "contact us" tier hiding the only pricing that matters. Lane is the entry tier — single gate, unlimited sessions, REST + webhooks. EV Lane adds OCPP for chargers. Business and Enterprise tiers add SSO, white-label, and a dedicated CSM.
Most operators
2.5% + $0.30 per txn
EV networks + multi-family
2.5% + $0.30 per txn
≥ 20 lanes
Custom take rate
≥ 200 lanes
Custom take rate + SLA
Hardware add-on: $499 one-time per lane kit (Intel mini-PC + 2 ONVIF/RTSP cameras, pre-configured). All tiers ship Stripe Connect Custom payouts to your bank.
10 — Start
$29 per gate per month, unlimited sessions. The same REST surface that scales to 4,000 operators at GA.