# Convova public API reference

Base URL `https://convova.app/api/public/v1`. Generated from the same OpenAPI document the API serves at
`https://convova.app/api/public/v1/openapi.json`; the interactive reference with request samples is at
`https://convova.app/docs/api`.

## Endpoints

### GET /events
List published events — The public catalog of published events.
Parameters: `offset` (query), `limit` (query)

### GET /events/{slug}
Get an event by slug — A single published event. 404 if it isn't published or doesn't exist.
Parameters: `slug` (path)

### GET /events/{slug}/contributors
List contributors (people) — The event's people (visible only). Role/capacity is carried per-row in `tags`.
Parameters: `slug` (path), `offset` (query), `limit` (query)

### GET /events/{slug}/contributors/{contributorId}
Get a contributor — A single visible contributor.
Parameters: `slug` (path), `contributorId` (path)

### GET /events/{slug}/organizations
List organizations — The event's organizations (visible only). Role/capacity is carried per-row in `tags`.
Parameters: `slug` (path), `offset` (query), `limit` (query)

### GET /events/{slug}/organizations/{organizationId}
Get an organization — A single visible organization.
Parameters: `slug` (path), `organizationId` (path)

### GET /events/{slug}/sessions
List sessions (schedule) — The schedule (visible only), ordered by start time. Each session nests its tags, contributors, and organizations.
Parameters: `slug` (path), `offset` (query), `limit` (query)

### GET /events/{slug}/sessions/{sessionId}
Get a session — A single visible session with its nested tags, contributors, and organizations.
Parameters: `slug` (path), `sessionId` (path)

### GET /events/{slug}/contributor-tags
List contributor tags — The contributor taxonomy (Speaker, Judge, …). Group contributors by these.
Parameters: `slug` (path)

### GET /events/{slug}/organization-tags
List organization tags — The organization taxonomy (Sponsor, Exhibitor, …). Group organizations by these.
Parameters: `slug` (path)

### GET /events/{slug}/session-tags
List session tags — The session taxonomy (tracks, rooms, formats, …).
Parameters: `slug` (path)

### GET /events/{slug}/ticket-types
List ticket types — The event's ticket categories (name + color).
Parameters: `slug` (path)

### GET /hosts/{hostSlug}
Get a host by slug — The host's public page: name, description, logo, website, and its published events, newest first. 404 if the host does not exist or is suspended.
Parameters: `hostSlug` (path)

### GET /events/{slug}/products
List products for sale — The event's visible products, each with its currently-active `price` (null = not currently priced). `onSale` = purchasable right now (sales window open, priced, not sold out). `perOrderMax` caps a ticket per order; `perTicketMax` caps an add-on per ticket. Pass `?include=<productId>` (repeatable) to add a hidden product the host linked to.
Parameters: `slug` (path)

### POST /events/{slug}/orders/quote
Quote a cart — What the cart would cost right now — validates the seller, sales windows, prices, per-order and per-ticket caps, add-on attachment, and sold-out state, and computes every total server-side. Pure: no writes. `tickets` is one entry per ticket unit, each with its `addOns`. Rate-limited per IP.
Parameters: `slug` (path)
Body: OrderQuoteRequest

### POST /events/{slug}/orders
Create an order → Checkout URL — Re-validates the cart, places the hold (quantity, capacity, promo code), and returns the Stripe-hosted Checkout URL to redirect the buyer to (card-only). A zero-total order skips Stripe and comes back `paid`. The buyer is the `buyerToken` from the checkout code, or the signed-in Convova session; without one the call is 401. The hold lasts 30 minutes; lightly capped per buyer email.
Parameters: `slug` (path)
Body: OrderCreateRequest

### POST /events/{slug}/checkout/start
Email the buyer a 6-digit code — Sends a code to the email so the buyer can prove it before any hold exists. One live code per email; a resend waits 60 seconds. Rate-limited per IP.
Parameters: `slug` (path)
Body: CheckoutStartRequest

### POST /events/{slug}/checkout/verify
Turn the code into a buyer token — Checks the code (three tries, ten minutes) and returns `buyerToken`, valid for one hour, to pass when creating the order.
Parameters: `slug` (path)
Body: CheckoutVerifyRequest

### POST /events/{slug}/orders/{orderId}/cancel
Release a pending order — The buyer left Stripe's page: the Checkout Session is expired and the hold released at once, instead of at the timer. A paid order is left alone.
Parameters: `slug` (path), `orderId` (path)

### POST /events/{slug}/coupons/validate
Validate a promo code against a cart — Same computation as the quote with `couponCode` required: returns the discounted quote, or a 400 with one message, "This code can't be used", for every kind of refusal. Advisory — the code is re-checked and reserved with the hold at order creation. Rate-limited per IP.
Parameters: `slug` (path)
Body: OrderQuoteRequest

### GET /events/{slug}/orders/{orderId}
Get order status — Status + totals + grouped line snapshots for one order. The unguessable order id is the read capability; no buyer PII is returned. `status` is pending | paid | expired | refunded (a pending order past `expiresAt` reads as expired).
Parameters: `slug` (path), `orderId` (path)

## Shapes

### PublicEvent
- `id`: string (uuid)
- `slug`: string
- `name`: string
- `description`: string,null
- `venue`: string,null
- `location`: string,null
- `startDate`: string (date-time)
- `endDate`: string (date-time)
- `coverImageUrl`: string (uri) | null
- `status`: string
- `host`: object | null
- `createdAt`: string (date-time)
- `updatedAt`: string (date-time)

### PublicContributor
- `id`: string (uuid)
- `eventId`: string (uuid)
- `name`: string
- `title`: string,null
- `organization`: string,null
- `bio`: string,null
- `avatarUrl`: string (uri) | null
- `website`: string (uri) | null
- `linkedin`: string,null
- `x`: string,null
- `visible`: boolean
- `createdAt`: string (date-time)
- `updatedAt`: string (date-time)
- `tags`: object[]

### PublicOrganization
- `id`: string (uuid)
- `eventId`: string (uuid)
- `name`: string
- `website`: string (uri) | null
- `logoUrl`: string (uri) | null
- `description`: string,null
- `boothNumber`: string,null
- `notes`: string,null
- `visible`: boolean
- `createdAt`: string (date-time)
- `updatedAt`: string (date-time)
- `tags`: object[]

### PublicSession
- `id`: string (uuid)
- `eventId`: string (uuid)
- `title`: string
- `description`: string,null
- `startTime`: string (date-time)
- `endTime`: string (date-time)
- `visible`: boolean
- `createdAt`: string (date-time)
- `updatedAt`: string (date-time)
- `tags`: object[]
- `contributors`: object[]
- `organizations`: object[]

### PublicTag
- `id`: string (uuid)
- `eventId`: string (uuid)
- `name`: string
- `color`: string,null
- `sortOrder`: integer

### PublicTicketType
- `id`: string (uuid)
- `eventId`: string (uuid)
- `name`: string
- `color`: string,null

### PublicProduct
- `id`: string (uuid)
- `kind`: string
- `ticketTypeId`: string (uuid) | null
- `attachesTo`: string (uuid)[]
- `name`: string
- `description`: string,null
- `imageUrl`: string,null
- `salesStartAt`: string (date-time) | null
- `salesEndAt`: string (date-time) | null
- `perOrderMax`: integer | null
- `perTicketMax`: integer
- `price`: object | null
- `soldOut`: boolean
- `onSale`: boolean

### PublicHost
- `name`: string
- `slug`: string
- `description`: string,null
- `logoUrl`: string (uri) | null
- `website`: string (uri) | null
- `events`: object[]

### OrderQuoteRequest
- `tickets`: object[]
- `couponCode`: string

### OrderCreateRequest
- `tickets`: object[]
- `couponCode`: string
- `buyerToken`: string

### PublicQuote
- `currency`: string
- `tickets`: object[]
- `couponCode`: string,null
- `subtotal`: integer
- `discountTotal`: integer
- `taxTotal`: integer
- `total`: integer

### PublicOrderCheckout
- `orderId`: string (uuid)
- `checkoutUrl`: string (uri) | null
- `expiresAt`: string (date-time) | null
- `status`: string

### PublicOrder
- `id`: string (uuid)
- `status`: string
- `currency`: string
- `subtotal`: integer
- `discountTotal`: integer
- `taxTotal`: integer
- `total`: integer
- `items`: object[]
- `ticketCount`: integer
- `expiresAt`: string (date-time) | null
- `createdAt`: string (date-time)

Nested shapes:

- A cart ticket is `{ productId, addOns: [{ productId, quantity }] }`; `addOns` may
  be empty.
- A quoted unit is `{ productId, name, unitAmount, discountAmount, subtotal }`; a
  quoted ticket adds `addOns`, an array of quoted units.
- In `OrderQuoteRequest` and `OrderCreateRequest` only `tickets` is required;
  `couponCode` is optional, so omit it rather than send an empty string, and a
  cart holds at most 20 ticket units.
- An order item is `{ description, unitAmount, quantity, subtotal }`.
- An event's `host` is `{ name, slug }` or `null`.
- A session's `contributors` are `{ id, name, title, organization, avatarUrl }`,
  its `organizations` are `{ id, name, logoUrl }`, and its `tags` are `PublicTag`
  objects.
- A tag on a contributor or organization is `{ tag: PublicTag, sortOrder }`.
- A product's `price` is `{ unitAmount, currency }` or `null` when nothing is on
  sale right now; `onSale` is true only when the sales window is open, there is a
  price, it is not sold out, and the event has not ended. `attachesTo` on an
  add-on lists the ticket type ids it can ride on; empty means all.
