# Build an event landing page

The Convova Public API **needs no authentication and is CORS-enabled** (`Access-Control-Allow-Origin: *`), so you can call it from a browser, a server, or a build step. Content endpoints are GET; the ticket storefront adds POSTs (quote, checkout code, create order, cancel).

**Base URL:** `https://convova.app/api/public/v1`

Every response is a `{ data, error, pagination }` envelope. Timestamps are ISO 8601 strings.

## The model in one line

A **contributor** is a *person*; an **organization** is an *organization*. Neither has a built-in role — their capacity (Speaker, Judge, Sponsor, Partner, …) comes from the **tags** attached to them. To render sections, fetch a tag taxonomy and group by tag.

## 1. Fetch the event

```
GET https://convova.app/api/public/v1/events/{slug}
```

Returns the event in `data`: `name`, `description`, `venue`, `location`, `startDate`, `endDate`, `coverImageUrl`. An unpublished or unknown slug returns **404**.

## 2. Fetch the schedule

```
GET https://convova.app/api/public/v1/events/{slug}/sessions
```

Each session includes its `tags` and the `contributors` and `organizations` appearing on it (already trimmed to display fields), ordered by `startTime`.

## 3. Render people and organizations by capacity

```
GET https://convova.app/api/public/v1/events/{slug}/contributors
GET https://convova.app/api/public/v1/events/{slug}/contributor-tags
```

Group contributors by tag and render each group under its tag name (e.g. people tagged `Keynote` in a hero, `Judge` in a judges grid). Render the **groups** in the order `/contributor-tags` returns them — that's the organizer's section order (each tag's own `sortOrder`); **within** a group, order by the `sortOrder` on each contributor's tag entry. Do the same for organizations with `/organizations` + `/organization-tags`.

## Pagination

List endpoints accept `?offset=` and `?limit=` (1–100). Without them you get a capped full list and `pagination: null`. With them, `pagination` is `{ offset, limit, total, hasMore }`.

## Errors

Unpublished or unknown → **404**; invalid query params → **400**. The error envelope is `{ data: null, error: "message", pagination: null }`.

## Complete example (JavaScript)

```js
const BASE = "https://convova.app/api/public/v1";

const getJSON = async (path) => {
  const res = await fetch(BASE + path);
  const body = await res.json();
  if (body.error) {
    throw new Error(body.error);
  }
  return body.data;
};

const buildLanding = async (slug) => {
  const [event, sessions, contributors, contributorTags] = await Promise.all([
    getJSON("/events/" + slug),
    getJSON("/events/" + slug + "/sessions"),
    getJSON("/events/" + slug + "/contributors"),
    getJSON("/events/" + slug + "/contributor-tags"),
  ]);

  // Group people by their tag (capacity).
  const byTag = new Map(contributorTags.map((t) => [t.id, { tag: t, people: [] }]));
  for (const person of contributors) {
    for (const { tag, sortOrder } of person.tags) {
      byTag.get(tag.id)?.people.push({ person, sortOrder });
    }
  }
  for (const group of byTag.values()) {
    group.people.sort((a, b) => a.sortOrder - b.sortOrder);
  }

  return { event, sessions, sections: [...byTag.values()] };
};
```

For exact field-level schemas, see the [OpenAPI spec](https://convova.app/api/public/v1/openapi.json) or the [API reference](https://convova.app/docs/api).